From c91a4a2b014eac8a732cd6597b450eef97874fe6 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Tue, 3 Jun 2025 11:21:50 +0800 Subject: [PATCH 01/18] docs(README): add DeepWiki link --- README.md | 1 + README_CN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 60e01722..4382255e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ go-version release test-coverage + Ask DeepWiki [中文](README_CN.md) diff --git a/README_CN.md b/README_CN.md index 8f51b374..2f2bff98 100644 --- a/README_CN.md +++ b/README_CN.md @@ -5,6 +5,7 @@ go-version release test-coverage + Ask DeepWiki [English](README.md) From 1324457082a43312f285177273d291a96c643c03 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Tue, 3 Jun 2025 14:22:07 +0800 Subject: [PATCH 02/18] docs(servers): add gin server example --- doc/examples/servers/gin/.keep | 0 doc/examples/servers/gin/go.mod | 44 ++++++++++++ doc/examples/servers/gin/go.sum | 104 +++++++++++++++++++++++++++++ doc/examples/servers/gin/main.go | 65 ++++++++++++++++++ doc/examples/servers/gin/server.go | 63 +++++++++++++++++ 5 files changed, 276 insertions(+) delete mode 100644 doc/examples/servers/gin/.keep create mode 100644 doc/examples/servers/gin/go.mod create mode 100644 doc/examples/servers/gin/go.sum create mode 100644 doc/examples/servers/gin/main.go create mode 100644 doc/examples/servers/gin/server.go diff --git a/doc/examples/servers/gin/.keep b/doc/examples/servers/gin/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/doc/examples/servers/gin/go.mod b/doc/examples/servers/gin/go.mod new file mode 100644 index 00000000..e32d56a4 --- /dev/null +++ b/doc/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/doc/examples/servers/gin/go.sum b/doc/examples/servers/gin/go.sum new file mode 100644 index 00000000..17da0161 --- /dev/null +++ b/doc/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/doc/examples/servers/gin/main.go b/doc/examples/servers/gin/main.go new file mode 100644 index 00000000..0b0fe204 --- /dev/null +++ b/doc/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/doc/examples/servers/gin/server.go b/doc/examples/servers/gin/server.go new file mode 100644 index 00000000..8dc9431e --- /dev/null +++ b/doc/examples/servers/gin/server.go @@ -0,0 +1,63 @@ +/* + * 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.TagArg(""), gs.TagArg("${http.server}")).AsServer() +} + +type HttpServerConfig struct { + Address string `value:"${addr:=0.0.0.0:9090}"` + ReadTimeout time.Duration `value:"${readTimeout:=5s}"` + WriteTimeout time.Duration `value:"${writeTimeout:=5s}"` +} + +type SimpleGinServer struct { + svr *http.Server +} + +func NewSimpleGinServer(e *gin.Engine, cfg HttpServerConfig) *SimpleGinServer { + return &SimpleGinServer{svr: &http.Server{ + Addr: cfg.Address, + Handler: e, + ReadTimeout: cfg.ReadTimeout, + WriteTimeout: cfg.WriteTimeout, + }} +} + +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) +} From 856a45a00955830b745f0a76cd3d12b7371a0983 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Tue, 3 Jun 2025 16:26:10 +0800 Subject: [PATCH 03/18] refactor(log): add testcases --- .github/workflows/test.yml | 3 +- .gitignore | 4 +- log/log.go | 8 +- log/log_level_test.go | 81 ++++++ log/log_refresh.go | 61 ++--- log/log_refresh_test.go | 495 +++++++++++++++++++++++++++++++++++++ log/log_test.go | 48 ++-- log/plugin.go | 16 +- log/plugin_appender.go | 8 +- log/plugin_layout.go | 8 +- log/plugin_layout_test.go | 12 - log/plugin_logger.go | 39 +-- log/plugin_logger_test.go | 34 +-- log/plugin_test.go | 106 +++++++- log/testdata/log.xml | 24 ++ 15 files changed, 817 insertions(+), 130 deletions(-) create mode 100644 log/log_level_test.go create mode 100644 log/log_refresh_test.go create mode 100644 log/testdata/log.xml 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..5f19d07f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ go.work.sum doc/examples/bookman/conf/ doc/examples/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/log/log.go b/log/log.go index 6d03af9e..a8098512 100644 --- a/log/log.go +++ b/log/log.go @@ -34,14 +34,14 @@ 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) { + 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) { + if tag.GetLogger().EnableLevel(DebugLevel) { Record(ctx, DebugLevel, tag, fn()...) } } @@ -76,7 +76,7 @@ func Fatal(ctx context.Context, tag *Tag, fields ...Field) { // 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) { + if !logger.EnableLevel(level) { return // Skip if the logger doesn't allow this level } @@ -110,5 +110,5 @@ func Record(ctx context.Context, level Level, tag *Tag, fields ...Field) { e.CtxString = ctxString e.CtxFields = ctxFields - logger.publish(e) + logger.Publish(e) } diff --git a/log/log_level_test.go b/log/log_level_test.go new file mode 100644 index 00000000..7f638ac0 --- /dev/null +++ b/log/log_level_test.go @@ -0,0 +1,81 @@ +/* + * 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" + "testing" + + "github.com/lvan100/go-assert" +) + +func TestParseLevel(t *testing.T) { + tests := []struct { + str string + want Level + wantErr error + }{ + { + str: "none", + want: NoneLevel, + }, + { + str: "trace", + want: TraceLevel, + }, + { + str: "debug", + want: DebugLevel, + }, + { + str: "info", + want: InfoLevel, + }, + { + str: "warn", + want: WarnLevel, + }, + { + str: "error", + want: ErrorLevel, + }, + { + str: "panic", + want: PanicLevel, + }, + { + str: "fatal", + want: FatalLevel, + }, + { + str: "unknown", + want: Level(-1), + wantErr: fmt.Errorf("invalid level unknown"), + }, + } + for _, tt := range tests { + got, err := ParseLevel(tt.str) + assert.That(t, got).Equal(tt.want) + assert.That(t, err).Equal(tt.wantErr) + if tt.str == "unknown" { + assert.ThatString(t, got.String()).Equal("INVALID") + } else { + assert.ThatString(t, got.String()).Equal(strings.ToUpper(tt.str)) + } + } +} diff --git a/log/log_refresh.go b/log/log_refresh.go index 089e6c59..79cbe2ae 100644 --- a/log/log_refresh.go +++ b/log/log_refresh.go @@ -38,9 +38,7 @@ func RefreshFile(fileName string) error { if err != nil { return err } - defer func() { - _ = file.Close() - }() + defer file.Close() ext := filepath.Ext(fileName) return RefreshReader(file, ext) } @@ -68,7 +66,7 @@ func RefreshReader(input io.Reader, ext string) error { } if rootNode.Label != "Configuration" { - return errors.New("RefreshReader: the Configuration root not found") + return errors.New("RefreshReader: Configuration root not found") } var ( @@ -82,26 +80,28 @@ func RefreshReader(input io.Reader, ext string) error { // Parse section nodes := rootNode.getChildren("Properties") if len(nodes) > 1 { - return errors.New("RefreshReader: section must be unique") + return errors.New("RefreshReader: Properties 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") + if len(nodes) == 1 { + for _, c := range nodes[0].Children { + if c.Label != "Property" { + continue + } + name, ok := c.Attributes["name"] + if !ok { + return fmt.Errorf("RefreshReader: attribute 'name' not found for node %s", c.Label) + } + properties[name] = c.Text } - properties[name] = c.Text } // Parse section nodes = rootNode.getChildren("Appenders") if len(nodes) == 0 { - return errors.New("RefreshReader: section not found") + return errors.New("RefreshReader: Appenders section not found") } if len(nodes) > 1 { - return errors.New("RefreshReader: section must be unique") + return errors.New("RefreshReader: Appenders section must be unique") } for _, c := range nodes[0].Children { p, ok := plugins[c.Label] @@ -122,10 +122,10 @@ func RefreshReader(input io.Reader, ext string) error { // Parse section nodes = rootNode.getChildren("Loggers") if len(nodes) == 0 { - return errors.New("RefreshReader: section not found") + return errors.New("RefreshReader: Loggers section not found") } if len(nodes) > 1 { - return errors.New("RefreshReader: section must be unique") + return errors.New("RefreshReader: Loggers section must be unique") } for _, c := range nodes[0].Children { isRootLogger := c.Label == "Root" || c.Label == "AsyncRoot" @@ -133,7 +133,7 @@ func RefreshReader(input io.Reader, ext string) error { if cRoot != nil { return errors.New("RefreshReader: found more than one root loggers") } - c.Attributes["name"] = "" + c.Attributes["name"] = "::root::" } p, ok := plugins[c.Label] @@ -142,7 +142,7 @@ func RefreshReader(input io.Reader, ext string) error { } name, ok := c.Attributes["name"] if !ok { - return errors.New("RefreshReader: attribute 'name' not found") + return fmt.Errorf("RefreshReader: attribute 'name' not found for node %s", c.Label) } v, err := NewPlugin(p.Class, c, properties) if err != nil { @@ -173,24 +173,27 @@ func RefreshReader(input io.Reader, ext string) error { if isRootLogger { if base.Tags != "" { - return fmt.Errorf("RefreshReader: root logger can not have tags attribute") + return fmt.Errorf("RefreshReader: root logger can not have attribute 'tags'") } } 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 { + var ss []string + for _, s := range strings.Split(base.Tags, ",") { if s = strings.TrimSpace(s); s == "" { - return fmt.Errorf("RefreshReader: logger tag can not be empty") + continue } + ss = append(ss, s) + } + if len(ss) == 0 { + return fmt.Errorf("RefreshReader: logger must have attribute 'tags' except root logger") + } + for _, s := range ss { cTags[s] = logger } } } if cRoot == nil { - return errors.New("found no root logger") + return errors.New("RefreshReader: found no root logger") } var ( @@ -209,12 +212,12 @@ func RefreshReader(input io.Reader, ext string) error { for _, a := range cAppenders { if err := a.Start(); err != nil { - return err + return errutil.WrapError(err, "RefreshReader: appender %s start error", a.GetName()) } } for _, l := range cLoggers { if err := l.Start(); err != nil { - return err + return errutil.WrapError(err, "RefreshReader: logger %s start error", l.GetName()) } } diff --git a/log/log_refresh_test.go b/log/log_refresh_test.go new file mode 100644 index 00000000..7c03c738 --- /dev/null +++ b/log/log_refresh_test.go @@ -0,0 +1,495 @@ +/* + * 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" + "strings" + "testing" + + "github.com/lvan100/go-assert" +) + +type funcReader func(p []byte) (n int, err error) + +func (r funcReader) Read(p []byte) (n int, err error) { + return r(p) +} + +func TestRefresh(t *testing.T) { + + t.Run("file not exist", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshFile("testdata/file-not-exist.xml") + assert.ThatError(t, err).Matches("open testdata/file-not-exist.xml") + }) + + t.Run("already refresh", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshFile("testdata/log.xml") + assert.Nil(t, err) + // ... + err = RefreshFile("testdata/log.xml") + assert.ThatError(t, err).Matches("RefreshReader: log refresh already done") + }) + + t.Run("unsupported file", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(nil, ".json") + assert.ThatError(t, err).Matches("RefreshReader: unsupported file type .json") + }) + + t.Run("read file error", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(funcReader(func(p []byte) (n int, err error) { + return 0, errors.New("read error") + }), ".xml") + assert.ThatError(t, err).Matches("read error") + }) + + t.Run("read node error - 1", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(""), ".xml") + assert.ThatError(t, err).Matches("invalid XML structure: missing root element") + }) + + t.Run("read node error - 2", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: Configuration root not found") + }) + + t.Run("more Properties", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: Properties section must be unique") + }) + + t.Run("error Property", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + abc + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: attribute 'name' not found for node Property") + }) + + t.Run("no Appenders", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: Appenders section not found") + }) + + t.Run("more Appenders", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: Appenders section must be unique") + }) + + t.Run("unfound Appender", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: plugin NotExistAppender not found") + }) + + t.Run("Appender error - 1", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: attribute 'name' not found") + }) + + t.Run("Appender error - 2", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("create plugin log.FileAppender error << found no plugin elements for struct field Layout") + }) + + t.Run("Appender error - 3", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("create plugin log.FileAppender error << found no attribute for struct field FileName") + }) + + t.Run("no Loggers", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: Loggers section not found") + }) + + t.Run("more Loggers", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: Loggers section must be unique") + }) + + t.Run("no Logger", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: found no root logger") + }) + + t.Run("Logger error - 1", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: plugin NotExistLogger not found") + }) + + t.Run("Logger error - 2", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: attribute 'name' not found for node Logger") + }) + + t.Run("Logger error - 3", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("create plugin log.LoggerConfig error << found no attribute for struct field Level") + }) + + t.Run("Logger error - 4", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: appender not-exist-appender not found") + }) + + t.Run("Logger error - 5", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: logger must have attribute 'tags' except root logger") + }) + + t.Run("Root error - 1", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: found no root logger") + }) + + t.Run("Root error - 2", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("create plugin log.LoggerConfig error << found no attribute for struct field Level") + }) + + t.Run("Root error - 3", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("create plugin log.LoggerConfig error << found no plugin elements for struct field AppenderRefs") + }) + + t.Run("Root error - 4", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: root logger can not have attribute 'tags'") + }) + + t.Run("Root error - 5", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: found more than one root loggers") + }) + + t.Run("tag error", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: `a\\[` regexp compile error << error parsing regexp: missing closing \\]: `\\[`") + }) + + t.Run("appender start error", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: appender file start error << open /not-exist-dir/access.log: no such file or directory") + }) + + t.Run("logger start error", func(t *testing.T) { + defer func() { initOnce.Store(false) }() + err := RefreshReader(strings.NewReader(` + + + + + + + + + + + + + + + + + `), ".xml") + assert.ThatError(t, err).Matches("RefreshReader: logger ::root:: start error << bufferSize is too small") + }) +} diff --git a/log/log_test.go b/log/log_test.go index f7256ef0..0252ce7c 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -18,8 +18,8 @@ package log_test import ( "context" - "strings" "testing" + "time" "github.com/go-spring/spring-core/log" "github.com/lvan100/go-assert" @@ -32,6 +32,10 @@ var TagRequestOut = log.GetTag("_com_request_out") func TestLog(t *testing.T) { ctx := t.Context() + log.TimeNow = func(ctx context.Context) time.Time { + return time.Now() + } + log.StringFromContext = func(ctx context.Context) string { return "" } @@ -54,42 +58,32 @@ func TestLog(t *testing.T) { 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") + err := log.RefreshFile("testdata/log.xml") assert.Nil(t, err) ctx = context.WithValue(ctx, "trace_id", "0a882193682db71edd48044db54cae88") ctx = context.WithValue(ctx, "span_id", "50ef0724418c0a66") + log.Trace(ctx, TagRequestOut, func() []log.Field { + return []log.Field{ + log.Msgf("hello %s", "world"), + } + }) + 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")) + log.Warn(ctx, TagRequestIn, log.Msgf("hello %s", "world")) + log.Error(ctx, TagRequestIn, log.Msgf("hello %s", "world")) + log.Panic(ctx, TagRequestIn, log.Msgf("hello %s", "world")) + log.Fatal(ctx, TagRequestIn, log.Msgf("hello %s", "world")) + + log.Info(ctx, TagDefault, log.Msgf("hello %s", "world")) + log.Warn(ctx, TagDefault, log.Msgf("hello %s", "world")) + log.Error(ctx, TagDefault, log.Msgf("hello %s", "world")) + log.Panic(ctx, TagDefault, log.Msgf("hello %s", "world")) } diff --git a/log/plugin.go b/log/plugin.go index 2a50b3e1..f1483d21 100644 --- a/log/plugin.go +++ b/log/plugin.go @@ -68,12 +68,15 @@ type Plugin struct { } // RegisterPlugin Registers a plugin with a given name and type. -func RegisterPlugin[T Lifecycle](name string, typ PluginType) { +func RegisterPlugin[T any](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() + t := reflect.TypeFor[T]() + if t.Kind() != reflect.Struct { + panic("T must be struct") + } plugins[name] = &Plugin{ Name: name, Type: typ, @@ -135,11 +138,10 @@ func (tag PluginTag) Lookup(key string) (value string, ok bool) { } 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 + if len(ss) != 2 { + return "", false + } else if ss[0] == key { + return ss[1], true } } return "", false diff --git a/log/plugin_appender.go b/log/plugin_appender.go index a7911659..b1852dca 100644 --- a/log/plugin_appender.go +++ b/log/plugin_appender.go @@ -21,14 +21,15 @@ import ( ) func init() { - RegisterPlugin[*DiscardAppender]("Discard", PluginTypeAppender) - RegisterPlugin[*ConsoleAppender]("Console", PluginTypeAppender) - RegisterPlugin[*FileAppender]("File", PluginTypeAppender) + 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 + GetName() string // Returns the appender name Append(e *Event) // Handles writing a log event } @@ -44,6 +45,7 @@ type BaseAppender struct { Layout Layout `PluginElement:"Layout"` // Layout defines how logs are formatted } +func (c *BaseAppender) GetName() string { return c.Name } func (c *BaseAppender) Start() error { return nil } func (c *BaseAppender) Stop() {} func (c *BaseAppender) Append(e *Event) {} diff --git a/log/plugin_layout.go b/log/plugin_layout.go index 0e8fd3aa..3136e7e9 100644 --- a/log/plugin_layout.go +++ b/log/plugin_layout.go @@ -32,8 +32,8 @@ var bytesSizeTable = map[string]int64{ func init() { RegisterConverter[HumanizeBytes](ParseHumanizeBytes) - RegisterPlugin[*TextLayout]("TextLayout", PluginTypeLayout) - RegisterPlugin[*JSONLayout]("JSONLayout", PluginTypeLayout) + RegisterPlugin[TextLayout]("TextLayout", PluginTypeLayout) + RegisterPlugin[JSONLayout]("JSONLayout", PluginTypeLayout) } type HumanizeBytes int @@ -62,7 +62,6 @@ func ParseHumanizeBytes(s string) (HumanizeBytes, error) { // Layout is the interface that defines how a log event is converted to bytes. type Layout interface { - Lifecycle ToBytes(e *Event) []byte } @@ -74,9 +73,6 @@ type BaseLayout struct { 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 { diff --git a/log/plugin_layout_test.go b/log/plugin_layout_test.go index bb783fe1..a8421ae4 100644 --- a/log/plugin_layout_test.go +++ b/log/plugin_layout_test.go @@ -100,10 +100,6 @@ func TestTextLayout(t *testing.T) { FileLineLength: 48, }, } - - err := layout.Start() - assert.Nil(t, err) - b := layout.ToBytes(&Event{ Level: InfoLevel, Time: time.Time{}, @@ -115,8 +111,6 @@ func TestTextLayout(t *testing.T) { 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() }) } @@ -128,10 +122,6 @@ func TestJSONLayout(t *testing.T) { FileLineLength: 48, }, } - - err := layout.Start() - assert.Nil(t, err) - b := layout.ToBytes(&Event{ Level: InfoLevel, Time: time.Time{}, @@ -143,7 +133,5 @@ func TestJSONLayout(t *testing.T) { 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 index 609c4295..f39761be 100644 --- a/log/plugin_logger.go +++ b/log/plugin_logger.go @@ -24,11 +24,11 @@ import ( 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) + 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. @@ -39,8 +39,9 @@ type Logger struct { // 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 + GetName() string // Get the name of the logger + 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, @@ -50,9 +51,6 @@ type AppenderRef struct { 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"` @@ -61,8 +59,10 @@ type baseLoggerConfig struct { AppenderRefs []*AppenderRef `PluginElement:"AppenderRef"` } -func (c *baseLoggerConfig) Start() error { return nil } -func (c *baseLoggerConfig) Stop() {} +// GetName returns the name of the logger. +func (c *baseLoggerConfig) GetName() string { + return c.Name +} // callAppenders sends the event to all configured appenders. func (c *baseLoggerConfig) callAppenders(e *Event) { @@ -71,8 +71,8 @@ func (c *baseLoggerConfig) callAppenders(e *Event) { } } -// enableLevel returns true if the specified log level is enabled. -func (c *baseLoggerConfig) enableLevel(level Level) bool { +// EnableLevel returns true if the specified log level is enabled. +func (c *baseLoggerConfig) EnableLevel(level Level) bool { return level >= c.Level } @@ -81,8 +81,11 @@ type LoggerConfig struct { baseLoggerConfig } -// publish sends the event directly to the appenders. -func (c *LoggerConfig) publish(e *Event) { +func (c *LoggerConfig) Start() error { return nil } +func (c *LoggerConfig) Stop() {} + +// Publish sends the event directly to the appenders. +func (c *LoggerConfig) Publish(e *Event) { c.callAppenders(e) PutEvent(e) } @@ -116,8 +119,8 @@ func (c *AsyncLoggerConfig) Start() error { return nil } -// publish places the event in the buffer if there's space; drops it otherwise. -func (c *AsyncLoggerConfig) publish(e *Event) { +// 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: diff --git a/log/plugin_logger_test.go b/log/plugin_logger_test.go index c2d4dd02..051482b5 100644 --- a/log/plugin_logger_test.go +++ b/log/plugin_logger_test.go @@ -54,16 +54,16 @@ func TestLoggerConfig(t *testing.T) { 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)) + 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{}) + l.Publish(&Event{}) } assert.That(t, a.count).Equal(5) @@ -82,13 +82,13 @@ func TestAsyncLoggerConfig(t *testing.T) { }, } - 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)) + 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) { @@ -134,7 +134,7 @@ func TestAsyncLoggerConfig(t *testing.T) { assert.Nil(t, err) for i := 0; i < 5000; i++ { - l.publish(GetEvent()) + l.Publish(GetEvent()) } time.Sleep(200 * time.Millisecond) @@ -168,7 +168,7 @@ func TestAsyncLoggerConfig(t *testing.T) { assert.Nil(t, err) for i := 0; i < 5; i++ { - l.publish(GetEvent()) + l.Publish(GetEvent()) } time.Sleep(100 * time.Millisecond) diff --git a/log/plugin_test.go b/log/plugin_test.go index da9f923f..25662386 100644 --- a/log/plugin_test.go +++ b/log/plugin_test.go @@ -25,8 +25,11 @@ import ( 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") + RegisterPlugin[int]("DummyLayout", PluginTypeLayout) + }, "T must be struct") + assert.Panic(t, func() { + RegisterPlugin[FileAppender]("File", PluginTypeAppender) + }, "duplicate plugin Appender in .*/plugin_appender.go:26 and .*/plugin_test.go:31") } func TestInjectAttribute(t *testing.T) { @@ -128,7 +131,7 @@ func TestInjectElement(t *testing.T) { 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) { + t.Run("plugin not found - 1", func(t *testing.T) { type ErrorPlugin struct { Layout Layout `PluginElement:"Layout"` } @@ -154,7 +157,33 @@ func TestInjectElement(t *testing.T) { 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) { + t.Run("plugin not found - 2", func(t *testing.T) { + type ErrorPlugin struct { + Layout Layout `PluginElement:"Layout,default"` + } + 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("plugin not found - 3", func(t *testing.T) { + type ErrorPlugin struct { + Layout Layout `PluginElement:"Layout,default=DummyLayout"` + } + typ := reflect.TypeFor[ErrorPlugin]() + _, err := NewPlugin(typ, &Node{ + Children: []*Node{ + {Label: "File"}, + }, + }, nil) + assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << plugin DummyLayout not found for struct field Layout") + }) + + t.Run("NewPlugin error - 1", func(t *testing.T) { type ErrorPlugin struct { Layout Layout `PluginElement:"Layout"` } @@ -172,4 +201,73 @@ func TestInjectElement(t *testing.T) { assert.ThatError(t, err).Matches(`create plugin log.ErrorPlugin error << create plugin log.TextLayout error ` + `<< inject struct field BufferSize error << unhandled size name: \"GB\"`) }) + + t.Run("NewPlugin error - 2", func(t *testing.T) { + type ErrorPlugin struct { + Appender Appender `PluginElement:"Appender,default=File"` + } + typ := reflect.TypeFor[ErrorPlugin]() + _, err := NewPlugin(typ, &Node{}, nil) + assert.ThatError(t, err).Matches(`create plugin log.ErrorPlugin error << create plugin log.FileAppender error ` + + `<< found no attribute for struct field Name`) + }) + + t.Run("NewPlugin error - 3", func(t *testing.T) { + type ErrorPlugin struct { + Layout Layout `PluginElement:"Layout"` + } + typ := reflect.TypeFor[ErrorPlugin]() + _, err := NewPlugin(typ, &Node{ + Children: []*Node{ + {Label: "TextLayout"}, + {Label: "TextLayout"}, + }, + }, nil) + assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << found 2 plugin elements for struct field Layout") + }) + + t.Run("NewPlugin error - 4", func(t *testing.T) { + type ErrorPlugin struct { + Layout map[string]Layout `PluginElement:"Layout"` + } + typ := reflect.TypeFor[ErrorPlugin]() + _, err := NewPlugin(typ, &Node{ + Children: []*Node{ + {Label: "TextLayout"}, + {Label: "TextLayout"}, + }, + }, nil) + assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << unsupported inject type map\\[string]log.Layout for struct field Layout") + }) + + t.Run("NewPlugin success - 1", func(t *testing.T) { + type ErrorPlugin struct { + Layout Layout `PluginElement:"Layout"` + } + typ := reflect.TypeFor[ErrorPlugin]() + _, err := NewPlugin(typ, &Node{ + Children: []*Node{ + {Label: "TextLayout"}, + }, + }, nil) + assert.Nil(t, err) + }) + + t.Run("NewPlugin success - 2", func(t *testing.T) { + type ErrorPlugin struct { + Layout Layout `PluginElement:"Layout,default=TextLayout"` + } + typ := reflect.TypeFor[ErrorPlugin]() + _, err := NewPlugin(typ, &Node{}, nil) + assert.Nil(t, err) + }) + + t.Run("NewPlugin success - 3", func(t *testing.T) { + type ErrorPlugin struct { + Layouts []Layout `PluginElement:"Layout,default=TextLayout"` + } + typ := reflect.TypeFor[ErrorPlugin]() + _, err := NewPlugin(typ, &Node{}, nil) + assert.Nil(t, err) + }) } diff --git a/log/testdata/log.xml b/log/testdata/log.xml new file mode 100644 index 00000000..8fd460a1 --- /dev/null +++ b/log/testdata/log.xml @@ -0,0 +1,24 @@ + + + + 100KB + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 5d506e6edceb138412b924dcc096fb18be5dd59e Mon Sep 17 00:00:00 2001 From: lvan100 Date: Tue, 3 Jun 2025 20:48:28 +0800 Subject: [PATCH 04/18] refactor(log): remove unnecessary go-loop package --- go.mod | 1 - go.sum | 2 -- log/log_refresh.go | 1 + log/log_test.go | 15 +++++++++------ 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 16abfab6..9295c256 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.24 require ( github.com/expr-lang/expr v1.17.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..77e1730d 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,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/log/log_refresh.go b/log/log_refresh.go index 79cbe2ae..922b8491 100644 --- a/log/log_refresh.go +++ b/log/log_refresh.go @@ -38,6 +38,7 @@ func RefreshFile(fileName string) error { if err != nil { return err } + // nolint: errcheck defer file.Close() ext := filepath.Ext(fileName) return RefreshReader(file, ext) diff --git a/log/log_test.go b/log/log_test.go index 0252ce7c..fca5a7b7 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -25,6 +25,9 @@ import ( "github.com/lvan100/go-assert" ) +const TraceID = "trace_id" +const SpanID = "span_id" + var TagDefault = log.GetTag("_def") var TagRequestIn = log.GetTag("_com_request_in") var TagRequestOut = log.GetTag("_com_request_out") @@ -41,11 +44,11 @@ func TestLog(t *testing.T) { } log.FieldsFromContext = func(ctx context.Context) []log.Field { - traceID, _ := ctx.Value("trace_id").(string) - spanID, _ := ctx.Value("span_id").(string) + traceID, _ := ctx.Value(TraceID).(string) + spanID, _ := ctx.Value(SpanID).(string) return []log.Field{ - log.String("trace_id", traceID), - log.String("span_id", spanID), + log.String(TraceID, traceID), + log.String(SpanID, spanID), } } @@ -61,8 +64,8 @@ func TestLog(t *testing.T) { err := log.RefreshFile("testdata/log.xml") assert.Nil(t, err) - ctx = context.WithValue(ctx, "trace_id", "0a882193682db71edd48044db54cae88") - ctx = context.WithValue(ctx, "span_id", "50ef0724418c0a66") + ctx = context.WithValue(ctx, TraceID, "0a882193682db71edd48044db54cae88") + ctx = context.WithValue(ctx, SpanID, "50ef0724418c0a66") log.Trace(ctx, TagRequestOut, func() []log.Field { return []log.Field{ From 911ee3c0f9a0a8d8efdf38f447f0423aca3a9784 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Sat, 7 Jun 2025 12:42:44 +0800 Subject: [PATCH 05/18] feat(gs): add RunWith and RunAsync method --- doc/examples/miniapi/main.go | 7 +- doc/examples/noweb/main.go | 10 +- gs/gs.go | 25 +- gs/internal/gs_app/app.go | 17 +- log/benchmarks/fields/encoder/encoder.go | 379 +++++++++++ log/benchmarks/fields/field-value/field.go | 634 ++++++++++++++++++ log/benchmarks/fields/go.mod | 3 + .../fields/value-interface/field.go | 422 ++++++++++++ .../fields/value-interface/value.go | 274 ++++++++ log/benchmarks/fields/value-struct/field.go | 422 ++++++++++++ log/benchmarks/fields/value-struct/value.go | 339 ++++++++++ 11 files changed, 2527 insertions(+), 5 deletions(-) create mode 100644 log/benchmarks/fields/encoder/encoder.go create mode 100644 log/benchmarks/fields/field-value/field.go create mode 100644 log/benchmarks/fields/go.mod create mode 100644 log/benchmarks/fields/value-interface/field.go create mode 100644 log/benchmarks/fields/value-interface/value.go create mode 100644 log/benchmarks/fields/value-struct/field.go create mode 100644 log/benchmarks/fields/value-struct/value.go diff --git a/doc/examples/miniapi/main.go b/doc/examples/miniapi/main.go index ef26a211..ca816b33 100644 --- a/doc/examples/miniapi/main.go +++ b/doc/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/main.go b/doc/examples/noweb/main.go index 79984432..5dcdb875 100644 --- a/doc/examples/noweb/main.go +++ b/doc/examples/noweb/main.go @@ -18,11 +18,19 @@ package main import ( "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() + stop, err := gs.Web(false).RunAsync() + if err != nil { + syslog.Errorf("app run failed: %s", err.Error()) + } + defer stop() + + syslog.Infof("app started") + select {} } // ~ telnet 127.0.0.1 9090 diff --git a/gs/gs.go b/gs/gs.go index 8b1997fd..767c0d08 100644 --- a/gs/gs.go +++ b/gs/gs.go @@ -215,6 +215,11 @@ func Web(enable bool) *AppStarter { // Run runs the app and waits for an interrupt signal to exit. func (s *AppStarter) Run() { + s.RunWith(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 { @@ -226,7 +231,15 @@ func (s *AppStarter) Run() { return } B = nil - err = gs_app.GS.Run() + 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 := gs_app.GS.Start(); err != nil { + return nil, err + } + return func() { gs_app.GS.Stop() }, nil } // Run runs the app and waits for an interrupt signal to exit. @@ -234,6 +247,16 @@ 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() diff --git a/gs/internal/gs_app/app.go b/gs/internal/gs_app/app.go index 9ec0b193..8e671cac 100644 --- a/gs/internal/gs_app/app.go +++ b/gs/internal/gs_app/app.go @@ -74,12 +74,24 @@ 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) @@ -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 { diff --git a/log/benchmarks/fields/encoder/encoder.go b/log/benchmarks/fields/encoder/encoder.go new file mode 100644 index 00000000..3f2a2c26 --- /dev/null +++ b/log/benchmarks/fields/encoder/encoder.go @@ -0,0 +1,379 @@ +/* + * 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 encoder + +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/benchmarks/fields/field-value/field.go b/log/benchmarks/fields/field-value/field.go new file mode 100644 index 00000000..31c30dfb --- /dev/null +++ b/log/benchmarks/fields/field-value/field.go @@ -0,0 +1,634 @@ +/* + * 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 value_struct + +import ( + "fmt" +) + +type ValueType int + +const ( + ValueTypeBool = ValueType(iota) + ValueTypeInt64 + ValueTypeUint64 + ValueTypeFloat64 + ValueTypeString + ValueTypeReflect + ValueTypeBools + ValueTypeInts + ValueTypeInt8s + ValueTypeInt16s + ValueTypeInt32s + ValueTypeInt64s + ValueTypeUints + ValueTypeUint8s + ValueTypeUint16s + ValueTypeUint32s + ValueTypeUint64s + ValueTypeFloat32s + ValueTypeFloat64s + ValueTypeStrings +) + +const MsgKey = "msg" + +// Field represents a structured log field with a key and a value. +type Field struct { + Key string + Type ValueType + Num uint64 + Str string + Any any +} + +// 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, + Type: ValueTypeReflect, + Any: 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Bools creates a Field with a slice of booleans. +func Bools(key string, val []bool) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Ints creates a Field with a slice of integers. +func Ints(key string, val []int) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Int8s creates a Field with a slice of int8 values. +func Int8s(key string, val []int8) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Int16s creates a Field with a slice of int16 values. +func Int16s(key string, val []int16) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Int32s creates a Field with a slice of int32 values. +func Int32s(key string, val []int32) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Int64s creates a Field with a slice of int64 values. +func Int64s(key string, val []int64) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Uints creates a Field with a slice of unsigned integers. +func Uints(key string, val []uint) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Uint8s creates a Field with a slice of uint8 values. +func Uint8s(key string, val []uint8) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Uint16s creates a Field with a slice of uint16 values. +func Uint16s(key string, val []uint16) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Uint32s creates a Field with a slice of uint32 values. +func Uint32s(key string, val []uint32) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Uint64s creates a Field with a slice of uint64 values. +func Uint64s(key string, val []uint64) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Float32s creates a Field with a slice of float32 values. +func Float32s(key string, val []float32) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Float64s creates a Field with a slice of float64 values. +func Float64s(key string, val []float64) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// Strings creates a Field with a slice of strings. +func Strings(key string, val []string) Field { + return Field{ + Key: key, + Type: 0, + Num: 0, + Str: "", + Any: nil, + } +} + +// 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/benchmarks/fields/go.mod b/log/benchmarks/fields/go.mod new file mode 100644 index 00000000..482bd27a --- /dev/null +++ b/log/benchmarks/fields/go.mod @@ -0,0 +1,3 @@ +module benchmark-fields + +go 1.24 diff --git a/log/benchmarks/fields/value-interface/field.go b/log/benchmarks/fields/value-interface/field.go new file mode 100644 index 00000000..749cbcda --- /dev/null +++ b/log/benchmarks/fields/value-interface/field.go @@ -0,0 +1,422 @@ +/* + * 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 value_interface + +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/benchmarks/fields/value-interface/value.go b/log/benchmarks/fields/value-interface/value.go new file mode 100644 index 00000000..67ac5098 --- /dev/null +++ b/log/benchmarks/fields/value-interface/value.go @@ -0,0 +1,274 @@ +/* + * 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 value_interface + +import ( + "fields/encoder" +) + +// Value is an interface for types that can encode themselves using an Encoder. +type Value interface { + Encode(enc encoder.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.Encoder) { + enc.AppendObjectBegin() + WriteFields(enc, v) + enc.AppendObjectEnd() +} + +// WriteFields writes a slice of Field objects to the encoder. +func WriteFields(enc encoder.Encoder, fields []Field) { + for _, f := range fields { + enc.AppendKey(f.Key) + f.Val.Encode(enc) + } +} diff --git a/log/benchmarks/fields/value-struct/field.go b/log/benchmarks/fields/value-struct/field.go new file mode 100644 index 00000000..adf32261 --- /dev/null +++ b/log/benchmarks/fields/value-struct/field.go @@ -0,0 +1,422 @@ +/* + * 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 value_struct + +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)} +} + +// 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(int64(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(int64(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(int64(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(int64(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(uint64(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(uint64(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(uint64(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(uint64(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(float64(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/benchmarks/fields/value-struct/value.go b/log/benchmarks/fields/value-struct/value.go new file mode 100644 index 00000000..e1fa6d78 --- /dev/null +++ b/log/benchmarks/fields/value-struct/value.go @@ -0,0 +1,339 @@ +/* + * 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 value_struct + +import ( + "math" + "unsafe" + + "fields/encoder" +) + +type ( + StringDataPtr *byte // used in Value.Any when the Value is a string +) + +type ValueType int + +const ( + ValueTypeBool = ValueType(iota) + ValueTypeInt64 + ValueTypeUint64 + ValueTypeFloat64 + ValueTypeString + ValueTypeReflect + ValueTypeBools + ValueTypeInts + ValueTypeInt8s + ValueTypeInt16s + ValueTypeInt32s + ValueTypeInt64s + ValueTypeUints + ValueTypeUint8s + ValueTypeUint16s + ValueTypeUint32s + ValueTypeUint64s + ValueTypeFloat32s + ValueTypeFloat64s + ValueTypeStrings +) + +type Value struct { + Type ValueType + Num uint64 + Any any +} + +func (v Value) Encode(enc encoder.Encoder) { + switch v.Type { + case ValueTypeBool: + if v.Num == 0 { + enc.AppendBool(false) + } else { + enc.AppendBool(true) + } + case ValueTypeInt64: + enc.AppendInt64(int64(v.Num)) + case ValueTypeUint64: + enc.AppendUint64(v.Num) + case ValueTypeFloat64: + enc.AppendFloat64(math.Float64frombits(v.Num)) + case ValueTypeString: + enc.AppendString(unsafe.String(v.Any.(StringDataPtr), v.Num)) + case ValueTypeReflect: + enc.AppendReflect(v.Any) + case ValueTypeBools: + enc.AppendArrayBegin() + for _, val := range v.Any.([]bool) { + enc.AppendBool(val) + } + enc.AppendArrayEnd() + case ValueTypeInts: + enc.AppendArrayBegin() + for _, val := range v.Any.([]int) { + enc.AppendInt64(int64(val)) + } + enc.AppendArrayEnd() + case ValueTypeInt8s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]int8) { + enc.AppendInt64(int64(val)) + } + enc.AppendArrayEnd() + case ValueTypeInt16s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]int16) { + enc.AppendInt64(int64(val)) + } + enc.AppendArrayEnd() + case ValueTypeInt32s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]int32) { + enc.AppendInt64(int64(val)) + } + enc.AppendArrayEnd() + case ValueTypeInt64s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]int64) { + enc.AppendInt64(val) + } + enc.AppendArrayEnd() + case ValueTypeUints: + enc.AppendArrayBegin() + for _, val := range v.Any.([]uint) { + enc.AppendUint64(uint64(val)) + } + enc.AppendArrayEnd() + case ValueTypeUint8s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]uint8) { + enc.AppendUint64(uint64(val)) + } + enc.AppendArrayEnd() + case ValueTypeUint16s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]uint16) { + enc.AppendUint64(uint64(val)) + } + enc.AppendArrayEnd() + case ValueTypeUint32s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]uint32) { + enc.AppendUint64(uint64(val)) + } + enc.AppendArrayEnd() + case ValueTypeUint64s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]uint64) { + enc.AppendUint64(val) + } + enc.AppendArrayEnd() + case ValueTypeFloat32s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]float32) { + enc.AppendFloat64(float64(val)) + } + enc.AppendArrayEnd() + case ValueTypeFloat64s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]float64) { + enc.AppendFloat64(val) + } + enc.AppendArrayEnd() + case ValueTypeStrings: + enc.AppendArrayBegin() + for _, val := range v.Any.([]string) { + enc.AppendString(val) + } + enc.AppendArrayEnd() + + default: // for linter + } +} + +func BoolValue(v bool) Value { + if v { + return Value{ + Type: ValueTypeBool, + Num: 1, + } + } else { + return Value{ + Type: ValueTypeBool, + Num: 0, + } + } +} + +func Int64Value(v int64) Value { + return Value{ + Type: ValueTypeInt64, + Num: uint64(v), + } +} + +func Uint64Value(v uint64) Value { + return Value{ + Type: ValueTypeUint64, + Num: v, + } +} + +func Float64Value(v float64) Value { + return Value{ + Type: ValueTypeFloat64, + Num: math.Float64bits(v), + } +} + +func StringValue(v string) Value { + return Value{ + Type: ValueTypeString, + Num: uint64(len(v)), + Any: StringDataPtr(unsafe.StringData(v)), + } +} + +func ReflectValue(v any) Value { + return Value{ + Type: ValueTypeReflect, + Any: v, + } +} + +func BoolsValue(v []bool) Value { + return Value{ + Type: ValueTypeBools, + Any: v, + } +} + +func IntsValue(v []int) Value { + return Value{ + Type: ValueTypeInts, + Any: v, + } +} + +func Int8sValue(v []int8) Value { + return Value{ + Type: ValueTypeInt8s, + Any: v, + } +} + +func Int16sValue(v []int16) Value { + return Value{ + Type: ValueTypeInt16s, + Any: v, + } +} + +func Int32sValue(v []int32) Value { + return Value{ + Type: ValueTypeInt32s, + Any: v, + } +} + +func Int64sValue(v []int64) Value { + return Value{ + Type: ValueTypeInt64s, + Any: v, + } +} + +func UintsValue(v []uint) Value { + return Value{ + Type: ValueTypeUints, + Any: v, + } +} + +func Uint8sValue(v []uint8) Value { + return Value{ + Type: ValueTypeUint8s, + Any: v, + } +} + +func Uint16sValue(v []uint16) Value { + return Value{ + Type: ValueTypeUint16s, + Any: v, + } +} + +func Uint32sValue(v []uint32) Value { + return Value{ + Type: ValueTypeUint32s, + Any: v, + } +} + +func Uint64sValue(v []uint64) Value { + return Value{ + Type: ValueTypeUint64s, + Any: v, + } +} + +func Float32sValue(v []float32) Value { + return Value{ + Type: ValueTypeFloat32s, + Any: v, + } +} + +func Float64sValue(v []float64) Value { + return Value{ + Type: ValueTypeFloat64s, + Any: v, + } +} + +func StringsValue(v []string) Value { + return Value{ + Type: ValueTypeStrings, + Any: v, + } +} + +type ArrayValue []Value + +func (v ArrayValue) Encode(enc encoder.Encoder) { + enc.AppendArrayBegin() + for _, val := range v { + val.Encode(enc) + } + enc.AppendArrayEnd() +} + +type ObjectValue []Field + +func (v ObjectValue) Encode(enc encoder.Encoder) { + enc.AppendObjectBegin() + WriteFields(enc, v) + enc.AppendObjectEnd() +} + +func WriteFields(enc encoder.Encoder, fields []Field) { + for _, f := range fields { + enc.AppendKey(f.Key) + f.Val.Encode(enc) + } +} From 03be4c2b0b16893f6959c4542f96c1b75d6c29e4 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Sat, 7 Jun 2025 12:52:07 +0800 Subject: [PATCH 06/18] feat(gs): add RunWith and RunAsync method --- doc/examples/noweb/main.go | 9 ++++++--- gs/gs.go | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/doc/examples/noweb/main.go b/doc/examples/noweb/main.go index 5dcdb875..20e5f0c9 100644 --- a/doc/examples/noweb/main.go +++ b/doc/examples/noweb/main.go @@ -17,20 +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. - stop, err := gs.Web(false).RunAsync() + stopApp, err := gs.Web(false).RunAsync() if err != nil { syslog.Errorf("app run failed: %s", err.Error()) } - defer stop() syslog.Infof("app started") - select {} + time.Sleep(time.Minute) + + stopApp() } // ~ telnet 127.0.0.1 9090 diff --git a/gs/gs.go b/gs/gs.go index 767c0d08..16fef869 100644 --- a/gs/gs.go +++ b/gs/gs.go @@ -236,6 +236,11 @@ func (s *AppStarter) RunWith(fn func(ctx context.Context) error) { // RunAsync runs the app asynchronously and returns a function to stop the app. func (s *AppStarter) RunAsync() (func(), error) { + printBanner() + if err := B.(*gs_app.BootImpl).Run(); err != nil { + return nil, err + } + B = nil if err := gs_app.GS.Start(); err != nil { return nil, err } From d77b410d0583b90c7c9448fecb333c913d2d9d98 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Sat, 7 Jun 2025 19:30:13 +0800 Subject: [PATCH 07/18] refactor(benchmark): add field & value benchmarks --- log/benchmarks/fields/field-value/field.go | 598 +++--------------- .../fields/field-value/field_test.go | 191 ++++++ .../fields/value-interface/field.go | 337 +--------- .../fields/value-interface/field_test.go | 68 ++ .../fields/value-interface/value.go | 130 +--- log/benchmarks/fields/value-struct/field.go | 337 +--------- .../fields/value-struct/field_test.go | 68 ++ log/benchmarks/fields/value-struct/value.go | 281 +++----- 8 files changed, 539 insertions(+), 1471 deletions(-) create mode 100644 log/benchmarks/fields/field-value/field_test.go create mode 100644 log/benchmarks/fields/value-interface/field_test.go create mode 100644 log/benchmarks/fields/value-struct/field_test.go diff --git a/log/benchmarks/fields/field-value/field.go b/log/benchmarks/fields/field-value/field.go index 31c30dfb..ceae4259 100644 --- a/log/benchmarks/fields/field-value/field.go +++ b/log/benchmarks/fields/field-value/field.go @@ -14,10 +14,12 @@ * limitations under the License. */ -package value_struct +package field_value import ( - "fmt" + "math" + + "benchmark-fields/encoder" ) type ValueType int @@ -25,54 +27,24 @@ type ValueType int const ( ValueTypeBool = ValueType(iota) ValueTypeInt64 - ValueTypeUint64 ValueTypeFloat64 ValueTypeString ValueTypeReflect ValueTypeBools - ValueTypeInts - ValueTypeInt8s - ValueTypeInt16s - ValueTypeInt32s ValueTypeInt64s - ValueTypeUints - ValueTypeUint8s - ValueTypeUint16s - ValueTypeUint32s - ValueTypeUint64s - ValueTypeFloat32s ValueTypeFloat64s ValueTypeStrings + ValueTypeArray + ValueTypeObject ) -const MsgKey = "msg" - -// Field represents a structured log field with a key and a value. +// Field represents a structured log field with a key and it's value. type Field struct { - Key string - Type ValueType - Num uint64 - Str string - Any any -} - -// 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, - Type: ValueTypeReflect, - Any: val, - } + Key string // field key + Type ValueType // value type + Num uint64 // numeric value + Str string // string value + Any any // other value } // Nil creates a Field with a nil value. @@ -82,289 +54,53 @@ func Nil(key string) Field { // Bool creates a Field for a boolean value. func Bool(key string, val bool) Field { - return Field{ - Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// 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) + if val { + return Field{ + Key: key, + Type: ValueTypeBool, + Num: 1, + } } - return Bool(key, *val) -} - -// Int creates a Field for an int value. -func Int(key string, val int) Field { return Field{ Key: key, - Type: 0, + Type: ValueTypeBool, Num: 0, - Str: "", - Any: nil, } } -// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, + Type: ValueTypeInt64, + Num: uint64(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, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, + Type: ValueTypeFloat64, + Num: math.Float64bits(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, - Type: 0, - Num: 0, - Str: "", - Any: nil, + Type: ValueTypeString, + Str: 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// Object creates a Field containing a variadic slice of Fields, treated as a nested object. -func Object(key string, fields ...Field) Field { +// Reflect wraps any value into a Field using reflection. +func Reflect(key string, val interface{}) Field { return Field{ Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, + Type: ValueTypeReflect, + Any: val, } } @@ -372,54 +108,8 @@ func Object(key string, fields ...Field) Field { func Bools(key string, val []bool) Field { return Field{ Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// Ints creates a Field with a slice of integers. -func Ints(key string, val []int) Field { - return Field{ - Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// Int8s creates a Field with a slice of int8 values. -func Int8s(key string, val []int8) Field { - return Field{ - Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// Int16s creates a Field with a slice of int16 values. -func Int16s(key string, val []int16) Field { - return Field{ - Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// Int32s creates a Field with a slice of int32 values. -func Int32s(key string, val []int32) Field { - return Field{ - Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, + Type: ValueTypeBools, + Any: val, } } @@ -427,98 +117,49 @@ func Int32s(key string, val []int32) Field { func Int64s(key string, val []int64) Field { return Field{ Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// Uints creates a Field with a slice of unsigned integers. -func Uints(key string, val []uint) Field { - return Field{ - Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// Uint8s creates a Field with a slice of uint8 values. -func Uint8s(key string, val []uint8) Field { - return Field{ - Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } -} - -// Uint16s creates a Field with a slice of uint16 values. -func Uint16s(key string, val []uint16) Field { - return Field{ - Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, + Type: ValueTypeInt64s, + Any: val, } } -// Uint32s creates a Field with a slice of uint32 values. -func Uint32s(key string, val []uint32) Field { +// Float64s creates a Field with a slice of float64 values. +func Float64s(key string, val []float64) Field { return Field{ Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, + Type: ValueTypeFloat64s, + Any: val, } } -// Uint64s creates a Field with a slice of uint64 values. -func Uint64s(key string, val []uint64) Field { +// Strings creates a Field with a slice of strings. +func Strings(key string, val []string) Field { return Field{ Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, + Type: ValueTypeStrings, + Any: val, } } -// Float32s creates a Field with a slice of float32 values. -func Float32s(key string, val []float32) Field { - return Field{ - Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, - } +// ArrayValue represents a slice of Values. +type ArrayValue interface { + EncodeArray(enc encoder.Encoder) } -// Float64s creates a Field with a slice of float64 values. -func Float64s(key string, val []float64) Field { +// Array creates a Field containing a variadic slice of Values, wrapped as an array. +func Array(key string, val ArrayValue) Field { return Field{ Key: key, - Type: 0, - Num: 0, - Str: "", - Any: nil, + Type: ValueTypeArray, + Any: val, } } -// Strings creates a Field with a slice of strings. -func Strings(key string, val []string) Field { +// 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, - Type: 0, - Num: 0, - Str: "", - Any: nil, + Type: ValueTypeObject, + Any: fields, } } @@ -529,106 +170,75 @@ 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) } } + +// Encode encodes the data represented by v to an Encoder. +func (f *Field) Encode(enc encoder.Encoder) { + enc.AppendKey(f.Key) + switch f.Type { + case ValueTypeBool: + enc.AppendBool(f.Num != 0) + case ValueTypeInt64: + enc.AppendInt64(int64(f.Num)) + case ValueTypeFloat64: + enc.AppendFloat64(math.Float64frombits(f.Num)) + case ValueTypeString: + enc.AppendString(f.Str) + case ValueTypeReflect: + enc.AppendReflect(f.Any) + case ValueTypeBools: + enc.AppendArrayBegin() + for _, val := range f.Any.([]bool) { + enc.AppendBool(val) + } + enc.AppendArrayEnd() + case ValueTypeInt64s: + enc.AppendArrayBegin() + for _, val := range f.Any.([]int64) { + enc.AppendInt64(val) + } + enc.AppendArrayEnd() + case ValueTypeFloat64s: + enc.AppendArrayBegin() + for _, val := range f.Any.([]float64) { + enc.AppendFloat64(val) + } + enc.AppendArrayEnd() + case ValueTypeStrings: + enc.AppendArrayBegin() + for _, val := range f.Any.([]string) { + enc.AppendString(val) + } + enc.AppendArrayEnd() + case ValueTypeArray: + enc.AppendArrayBegin() + f.Any.(ArrayValue).EncodeArray(enc) + enc.AppendArrayEnd() + case ValueTypeObject: + enc.AppendObjectBegin() + for _, val := range f.Any.([]Field) { + val.Encode(enc) + } + enc.AppendObjectEnd() + default: // for linter + } +} diff --git a/log/benchmarks/fields/field-value/field_test.go b/log/benchmarks/fields/field-value/field_test.go new file mode 100644 index 00000000..6870622e --- /dev/null +++ b/log/benchmarks/fields/field-value/field_test.go @@ -0,0 +1,191 @@ +/* + * 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 field_value + +import ( + "bytes" + "fmt" + "testing" + + "benchmark-fields/encoder" +) + +type bools []bool + +func (arr bools) EncodeArray(enc encoder.Encoder) { + for _, v := range arr { + enc.AppendBool(v) + } +} + +type int64s []int64 + +func (arr int64s) EncodeArray(enc encoder.Encoder) { + for _, v := range arr { + enc.AppendInt64(v) + } +} + +type float64s []float64 + +func (arr float64s) EncodeArray(enc encoder.Encoder) { + for _, v := range arr { + enc.AppendFloat64(v) + } +} + +type strings []string + +func (arr strings) EncodeArray(enc encoder.Encoder) { + for _, v := range arr { + enc.AppendString(v) + } +} + +func BenchmarkBools(b *testing.B) { + + // bools-8 11279228 108.3 ns/op + // bools_as_ArrayValue-8 11232891 108.0 ns/op + // int64s-8 8994212 131.2 ns/op + // int64s_as_ArrayValue-8 9007236 131.8 ns/op + // float64s-8 1994095 600.4 ns/op + // float64s_as_ArrayValue-8 1985868 605.9 ns/op + // strings-8 8231821 145.5 ns/op + // strings_as_ArrayValue-8 8147164 148.5 ns/op + + arrBools := []bool{true, false, true, false, true, false} + arrInt64s := []int64{1, 2, 3, 4, 5, 6, 7, 8} + arrFloat64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8} + arrStrings := []string{"a", "b", "c", "d", "e", "f", "g", "h"} + + b1 := bytes.NewBuffer(nil) + b2 := bytes.NewBuffer(nil) + b3 := bytes.NewBuffer(nil) + b4 := bytes.NewBuffer(nil) + b5 := bytes.NewBuffer(nil) + b6 := bytes.NewBuffer(nil) + b7 := bytes.NewBuffer(nil) + b8 := bytes.NewBuffer(nil) + + { + v := Bools("arr", arrBools) + v.Encode(encoder.NewJSONEncoder(b1)) + } + + { + v := Array("arr", bools(arrBools)) + v.Encode(encoder.NewJSONEncoder(b2)) + } + + { + v := Int64s("arr", arrInt64s) + v.Encode(encoder.NewJSONEncoder(b3)) + } + + { + v := Array("arr", int64s(arrInt64s)) + v.Encode(encoder.NewJSONEncoder(b4)) + } + + { + v := Float64s("arr", arrFloat64s) + v.Encode(encoder.NewJSONEncoder(b5)) + } + + { + v := Array("arr", float64s(arrFloat64s)) + v.Encode(encoder.NewJSONEncoder(b6)) + } + + { + v := Strings("arr", arrStrings) + v.Encode(encoder.NewJSONEncoder(b7)) + } + + { + v := Array("arr", strings(arrStrings)) + v.Encode(encoder.NewJSONEncoder(b8)) + } + + fmt.Println(b1.String()) + fmt.Println(b2.String()) + fmt.Println(b3.String()) + fmt.Println(b4.String()) + fmt.Println(b5.String()) + fmt.Println(b6.String()) + fmt.Println(b7.String()) + fmt.Println(b8.String()) + + b.ResetTimer() + b.ReportAllocs() + + b.Run("bools", func(b *testing.B) { + for b.Loop() { + v := Bools("arr", arrBools) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + + b.Run("bools as ArrayValue", func(b *testing.B) { + for b.Loop() { + v := Array("arr", bools(arrBools)) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + + b.Run("int64s", func(b *testing.B) { + for b.Loop() { + v := Int64s("arr", arrInt64s) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + + b.Run("int64s as ArrayValue", func(b *testing.B) { + for b.Loop() { + v := Array("arr", int64s(arrInt64s)) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + + b.Run("float64s", func(b *testing.B) { + for b.Loop() { + v := Float64s("arr", arrFloat64s) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + + b.Run("float64s as ArrayValue", func(b *testing.B) { + for b.Loop() { + v := Array("arr", float64s(arrFloat64s)) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + + b.Run("strings", func(b *testing.B) { + for b.Loop() { + v := Strings("arr", arrStrings) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + + b.Run("strings as ArrayValue", func(b *testing.B) { + for b.Loop() { + v := Array("arr", strings(arrStrings)) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) +} diff --git a/log/benchmarks/fields/value-interface/field.go b/log/benchmarks/fields/value-interface/field.go index 749cbcda..a358ab74 100644 --- a/log/benchmarks/fields/value-interface/field.go +++ b/log/benchmarks/fields/value-interface/field.go @@ -16,33 +16,12 @@ package value_interface -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) @@ -53,191 +32,24 @@ 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)} +// Reflect wraps any value into a Field using reflection. +func Reflect(key string, val interface{}) Field { + return Field{Key: key, Val: ReflectValue{Val: val}} } // Bools creates a Field with a slice of booleans. @@ -245,61 +57,11 @@ 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)} @@ -310,6 +72,16 @@ func Strings(key string, val []string) Field { return Field{Key: key, Val: StringsValue(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)} +} + // 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. @@ -317,105 +89,22 @@ 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/benchmarks/fields/value-interface/field_test.go b/log/benchmarks/fields/value-interface/field_test.go new file mode 100644 index 00000000..c30fd891 --- /dev/null +++ b/log/benchmarks/fields/value-interface/field_test.go @@ -0,0 +1,68 @@ +/* + * 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 value_interface + +import ( + "bytes" + "testing" + + "benchmark-fields/encoder" +) + +func BenchmarkBools(b *testing.B) { + + // bools-8 11332849 107.7 ns/op + // int64s-8 9080331 129.8 ns/op + // float64s-8 2051444 583.3 ns/op + // strings-8 8384014 142.4 ns/op + + arrBools := []bool{true, false, true, false, true, false} + arrInt64s := []int64{1, 2, 3, 4, 5, 6, 7, 8} + arrFloat64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8} + arrStrings := []string{"a", "b", "c", "d", "e", "f", "g", "h"} + + b.ResetTimer() + b.ReportAllocs() + + b.Run("bools", func(b *testing.B) { + for b.Loop() { + v := Bools("arr", arrBools) + WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + } + }) + + b.Run("int64s", func(b *testing.B) { + for b.Loop() { + v := Int64s("arr", arrInt64s) + WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + } + }) + + b.Run("float64s", func(b *testing.B) { + for b.Loop() { + v := Float64s("arr", arrFloat64s) + WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + } + }) + + b.Run("strings", func(b *testing.B) { + for b.Loop() { + v := Strings("arr", arrStrings) + WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + } + }) +} diff --git a/log/benchmarks/fields/value-interface/value.go b/log/benchmarks/fields/value-interface/value.go index 67ac5098..31425c80 100644 --- a/log/benchmarks/fields/value-interface/value.go +++ b/log/benchmarks/fields/value-interface/value.go @@ -17,7 +17,7 @@ package value_interface import ( - "fields/encoder" + "benchmark-fields/encoder" ) // Value is an interface for types that can encode themselves using an Encoder. @@ -41,14 +41,6 @@ func (v Int64Value) Encode(enc encoder.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.Encoder) { - enc.AppendUint64(uint64(v)) -} - // Float64Value represents a float64 carried by Field. type Float64Value float64 @@ -87,54 +79,6 @@ func (v BoolsValue) Encode(enc encoder.Encoder) { 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.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.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.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.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 @@ -147,78 +91,6 @@ func (v Int64sValue) Encode(enc encoder.Encoder) { 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.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.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.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.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.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.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 diff --git a/log/benchmarks/fields/value-struct/field.go b/log/benchmarks/fields/value-struct/field.go index adf32261..e75e78af 100644 --- a/log/benchmarks/fields/value-struct/field.go +++ b/log/benchmarks/fields/value-struct/field.go @@ -16,33 +16,12 @@ package value_struct -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)} -} - // Nil creates a Field with a nil value. func Nil(key string) Field { return Reflect(key, nil) @@ -53,191 +32,24 @@ 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(int64(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(int64(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(int64(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(int64(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(uint64(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(uint64(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(uint64(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(uint64(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(float64(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)} +// Reflect wraps any value into a Field using reflection. +func Reflect(key string, val interface{}) Field { + return Field{Key: key, Val: ReflectValue(val)} } // Bools creates a Field with a slice of booleans. @@ -245,61 +57,11 @@ 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)} @@ -310,6 +72,16 @@ func Strings(key string, val []string) Field { return Field{Key: key, Val: StringsValue(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...)} +} + // 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. @@ -317,105 +89,22 @@ 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/benchmarks/fields/value-struct/field_test.go b/log/benchmarks/fields/value-struct/field_test.go new file mode 100644 index 00000000..730f413e --- /dev/null +++ b/log/benchmarks/fields/value-struct/field_test.go @@ -0,0 +1,68 @@ +/* + * 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 value_struct + +import ( + "bytes" + "testing" + + "benchmark-fields/encoder" +) + +func BenchmarkBools(b *testing.B) { + + // bools-8 11117685 105.6 ns/op + // int64s-8 9201427 136.9 ns/op + // float64s-8 1878270 618.2 ns/op + // strings-8 8103530 146.1 ns/op + + arrBools := []bool{true, false, true, false, true, false} + arrInt64s := []int64{1, 2, 3, 4, 5, 6, 7, 8} + arrFloat64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8} + arrStrings := []string{"a", "b", "c", "d", "e", "f", "g", "h"} + + b.ResetTimer() + b.ReportAllocs() + + b.Run("bools", func(b *testing.B) { + for b.Loop() { + v := Bools("arr", arrBools) + WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + } + }) + + b.Run("int64s", func(b *testing.B) { + for b.Loop() { + v := Int64s("arr", arrInt64s) + WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + } + }) + + b.Run("float64s", func(b *testing.B) { + for b.Loop() { + v := Float64s("arr", arrFloat64s) + WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + } + }) + + b.Run("strings", func(b *testing.B) { + for b.Loop() { + v := Strings("arr", arrStrings) + WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + } + }) +} diff --git a/log/benchmarks/fields/value-struct/value.go b/log/benchmarks/fields/value-struct/value.go index e1fa6d78..1fc1e709 100644 --- a/log/benchmarks/fields/value-struct/value.go +++ b/log/benchmarks/fields/value-struct/value.go @@ -20,7 +20,7 @@ import ( "math" "unsafe" - "fields/encoder" + "benchmark-fields/encoder" ) type ( @@ -32,24 +32,15 @@ type ValueType int const ( ValueTypeBool = ValueType(iota) ValueTypeInt64 - ValueTypeUint64 ValueTypeFloat64 ValueTypeString ValueTypeReflect ValueTypeBools - ValueTypeInts - ValueTypeInt8s - ValueTypeInt16s - ValueTypeInt32s ValueTypeInt64s - ValueTypeUints - ValueTypeUint8s - ValueTypeUint16s - ValueTypeUint32s - ValueTypeUint64s - ValueTypeFloat32s ValueTypeFloat64s ValueTypeStrings + ValueTypeArray + ValueTypeObject ) type Value struct { @@ -58,113 +49,7 @@ type Value struct { Any any } -func (v Value) Encode(enc encoder.Encoder) { - switch v.Type { - case ValueTypeBool: - if v.Num == 0 { - enc.AppendBool(false) - } else { - enc.AppendBool(true) - } - case ValueTypeInt64: - enc.AppendInt64(int64(v.Num)) - case ValueTypeUint64: - enc.AppendUint64(v.Num) - case ValueTypeFloat64: - enc.AppendFloat64(math.Float64frombits(v.Num)) - case ValueTypeString: - enc.AppendString(unsafe.String(v.Any.(StringDataPtr), v.Num)) - case ValueTypeReflect: - enc.AppendReflect(v.Any) - case ValueTypeBools: - enc.AppendArrayBegin() - for _, val := range v.Any.([]bool) { - enc.AppendBool(val) - } - enc.AppendArrayEnd() - case ValueTypeInts: - enc.AppendArrayBegin() - for _, val := range v.Any.([]int) { - enc.AppendInt64(int64(val)) - } - enc.AppendArrayEnd() - case ValueTypeInt8s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]int8) { - enc.AppendInt64(int64(val)) - } - enc.AppendArrayEnd() - case ValueTypeInt16s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]int16) { - enc.AppendInt64(int64(val)) - } - enc.AppendArrayEnd() - case ValueTypeInt32s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]int32) { - enc.AppendInt64(int64(val)) - } - enc.AppendArrayEnd() - case ValueTypeInt64s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]int64) { - enc.AppendInt64(val) - } - enc.AppendArrayEnd() - case ValueTypeUints: - enc.AppendArrayBegin() - for _, val := range v.Any.([]uint) { - enc.AppendUint64(uint64(val)) - } - enc.AppendArrayEnd() - case ValueTypeUint8s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]uint8) { - enc.AppendUint64(uint64(val)) - } - enc.AppendArrayEnd() - case ValueTypeUint16s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]uint16) { - enc.AppendUint64(uint64(val)) - } - enc.AppendArrayEnd() - case ValueTypeUint32s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]uint32) { - enc.AppendUint64(uint64(val)) - } - enc.AppendArrayEnd() - case ValueTypeUint64s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]uint64) { - enc.AppendUint64(val) - } - enc.AppendArrayEnd() - case ValueTypeFloat32s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]float32) { - enc.AppendFloat64(float64(val)) - } - enc.AppendArrayEnd() - case ValueTypeFloat64s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]float64) { - enc.AppendFloat64(val) - } - enc.AppendArrayEnd() - case ValueTypeStrings: - enc.AppendArrayBegin() - for _, val := range v.Any.([]string) { - enc.AppendString(val) - } - enc.AppendArrayEnd() - - default: // for linter - } -} - +// BoolValue represents a bool carried by Field. func BoolValue(v bool) Value { if v { return Value{ @@ -179,6 +64,7 @@ func BoolValue(v bool) Value { } } +// Int64Value represents an int64 carried by Field. func Int64Value(v int64) Value { return Value{ Type: ValueTypeInt64, @@ -186,13 +72,7 @@ func Int64Value(v int64) Value { } } -func Uint64Value(v uint64) Value { - return Value{ - Type: ValueTypeUint64, - Num: v, - } -} - +// Float64Value represents a float64 carried by Field. func Float64Value(v float64) Value { return Value{ Type: ValueTypeFloat64, @@ -200,6 +80,7 @@ func Float64Value(v float64) Value { } } +// StringValue represents a string carried by Field. func StringValue(v string) Value { return Value{ Type: ValueTypeString, @@ -208,6 +89,7 @@ func StringValue(v string) Value { } } +// ReflectValue represents an interface{} carried by Field. func ReflectValue(v any) Value { return Value{ Type: ValueTypeReflect, @@ -215,6 +97,7 @@ func ReflectValue(v any) Value { } } +// BoolsValue represents a slice of bool carried by Field. func BoolsValue(v []bool) Value { return Value{ Type: ValueTypeBools, @@ -222,34 +105,7 @@ func BoolsValue(v []bool) Value { } } -func IntsValue(v []int) Value { - return Value{ - Type: ValueTypeInts, - Any: v, - } -} - -func Int8sValue(v []int8) Value { - return Value{ - Type: ValueTypeInt8s, - Any: v, - } -} - -func Int16sValue(v []int16) Value { - return Value{ - Type: ValueTypeInt16s, - Any: v, - } -} - -func Int32sValue(v []int32) Value { - return Value{ - Type: ValueTypeInt32s, - Any: v, - } -} - +// Int64sValue represents a slice of int64 carried by Field. func Int64sValue(v []int64) Value { return Value{ Type: ValueTypeInt64s, @@ -257,48 +113,7 @@ func Int64sValue(v []int64) Value { } } -func UintsValue(v []uint) Value { - return Value{ - Type: ValueTypeUints, - Any: v, - } -} - -func Uint8sValue(v []uint8) Value { - return Value{ - Type: ValueTypeUint8s, - Any: v, - } -} - -func Uint16sValue(v []uint16) Value { - return Value{ - Type: ValueTypeUint16s, - Any: v, - } -} - -func Uint32sValue(v []uint32) Value { - return Value{ - Type: ValueTypeUint32s, - Any: v, - } -} - -func Uint64sValue(v []uint64) Value { - return Value{ - Type: ValueTypeUint64s, - Any: v, - } -} - -func Float32sValue(v []float32) Value { - return Value{ - Type: ValueTypeFloat32s, - Any: v, - } -} - +// Float64sValue represents a slice of float64 carried by Field. func Float64sValue(v []float64) Value { return Value{ Type: ValueTypeFloat64s, @@ -306,6 +121,7 @@ func Float64sValue(v []float64) Value { } } +// StringsValue represents a slice of string carried by Field. func StringsValue(v []string) Value { return Value{ Type: ValueTypeStrings, @@ -313,9 +129,17 @@ func StringsValue(v []string) Value { } } -type ArrayValue []Value +type arrVal []Value -func (v ArrayValue) Encode(enc encoder.Encoder) { +// ArrayValue represents a slice of Value carried by Field. +func ArrayValue(val ...Value) Value { + return Value{ + Type: ValueTypeArray, + Any: arrVal(val), + } +} + +func (v arrVal) Encode(enc encoder.Encoder) { enc.AppendArrayBegin() for _, val := range v { val.Encode(enc) @@ -323,9 +147,17 @@ func (v ArrayValue) Encode(enc encoder.Encoder) { enc.AppendArrayEnd() } -type ObjectValue []Field +type objVal []Field + +// ObjectValue represents a slice of Field carried by Field. +func ObjectValue(fields ...Field) Value { + return Value{ + Type: ValueTypeObject, + Any: objVal(fields), + } +} -func (v ObjectValue) Encode(enc encoder.Encoder) { +func (v objVal) Encode(enc encoder.Encoder) { enc.AppendObjectBegin() WriteFields(enc, v) enc.AppendObjectEnd() @@ -337,3 +169,52 @@ func WriteFields(enc encoder.Encoder, fields []Field) { f.Val.Encode(enc) } } + +// Encode encodes the Value to the encoder. +func (v Value) Encode(enc encoder.Encoder) { + switch v.Type { + case ValueTypeBool: + if v.Num == 0 { + enc.AppendBool(false) + } else { + enc.AppendBool(true) + } + case ValueTypeInt64: + enc.AppendInt64(int64(v.Num)) + case ValueTypeFloat64: + enc.AppendFloat64(math.Float64frombits(v.Num)) + case ValueTypeString: + enc.AppendString(unsafe.String(v.Any.(StringDataPtr), v.Num)) + case ValueTypeReflect: + enc.AppendReflect(v.Any) + case ValueTypeBools: + enc.AppendArrayBegin() + for _, val := range v.Any.([]bool) { + enc.AppendBool(val) + } + enc.AppendArrayEnd() + case ValueTypeInt64s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]int64) { + enc.AppendInt64(val) + } + enc.AppendArrayEnd() + case ValueTypeFloat64s: + enc.AppendArrayBegin() + for _, val := range v.Any.([]float64) { + enc.AppendFloat64(val) + } + enc.AppendArrayEnd() + case ValueTypeStrings: + enc.AppendArrayBegin() + for _, val := range v.Any.([]string) { + enc.AppendString(val) + } + enc.AppendArrayEnd() + case ValueTypeArray: + v.Any.(arrVal).Encode(enc) + case ValueTypeObject: + v.Any.(objVal).Encode(enc) + default: // for linter + } +} From ae41f276b6cb717a99d415280a36b86b98beb13e Mon Sep 17 00:00:00 2001 From: lvan100 Date: Mon, 9 Jun 2025 10:34:38 +0800 Subject: [PATCH 08/18] refactor(benchmark): add fields benchmarks --- log/benchmarks/fields/encoder/encoder.go | 219 +----------------- .../fields/field-value/field_test.go | 45 +++- log/benchmarks/fields/main.go | 19 ++ log/benchmarks/fields/main_test.go | 170 ++++++++++++++ .../fields/value-interface/field.go | 9 + .../fields/value-interface/field_test.go | 33 ++- .../fields/value-interface/value.go | 12 +- log/benchmarks/fields/value-struct/field.go | 9 + .../fields/value-struct/field_test.go | 33 ++- log/benchmarks/fields/value-struct/value.go | 11 +- 10 files changed, 291 insertions(+), 269 deletions(-) create mode 100644 log/benchmarks/fields/main.go create mode 100644 log/benchmarks/fields/main_test.go diff --git a/log/benchmarks/fields/encoder/encoder.go b/log/benchmarks/fields/encoder/encoder.go index 3f2a2c26..280068cf 100644 --- a/log/benchmarks/fields/encoder/encoder.go +++ b/log/benchmarks/fields/encoder/encoder.go @@ -20,7 +20,6 @@ import ( "bytes" "encoding/json" "strconv" - "unicode/utf8" ) // Encoder is an interface that defines methods for appending structured data elements. @@ -34,17 +33,11 @@ type Encoder interface { 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 @@ -123,7 +116,7 @@ func (enc *JSONEncoder) AppendKey(key string) { enc.appendSeparator() enc.last = jsonTokenKey enc.buf.WriteByte('"') - enc.safeAddString(key) + enc.buf.WriteString(key) enc.buf.WriteByte('"') enc.buf.WriteByte(':') } @@ -142,13 +135,6 @@ func (enc *JSONEncoder) AppendInt64(v int64) { 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() @@ -161,7 +147,7 @@ func (enc *JSONEncoder) AppendString(v string) { enc.appendSeparator() enc.last = jsonTokenValue enc.buf.WriteByte('"') - enc.safeAddString(v) + enc.buf.WriteString(v) enc.buf.WriteByte('"') } @@ -172,208 +158,9 @@ func (enc *JSONEncoder) AppendReflect(v interface{}) { b, err := json.Marshal(v) if err != nil { enc.buf.WriteByte('"') - enc.safeAddString(err.Error()) + enc.buf.WriteString(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/benchmarks/fields/field-value/field_test.go b/log/benchmarks/fields/field-value/field_test.go index 6870622e..f06ddd1e 100644 --- a/log/benchmarks/fields/field-value/field_test.go +++ b/log/benchmarks/fields/field-value/field_test.go @@ -56,16 +56,16 @@ func (arr strings) EncodeArray(enc encoder.Encoder) { } } -func BenchmarkBools(b *testing.B) { +func BenchmarkFieldValue(b *testing.B) { - // bools-8 11279228 108.3 ns/op - // bools_as_ArrayValue-8 11232891 108.0 ns/op - // int64s-8 8994212 131.2 ns/op - // int64s_as_ArrayValue-8 9007236 131.8 ns/op - // float64s-8 1994095 600.4 ns/op - // float64s_as_ArrayValue-8 1985868 605.9 ns/op - // strings-8 8231821 145.5 ns/op - // strings_as_ArrayValue-8 8147164 148.5 ns/op + // bools-8 10706354 123.8 ns/op 152 B/op 4 allocs/op + // bools_as_ArrayValue-8 11192071 107.9 ns/op 152 B/op 4 allocs/op + // int64s-8 9132631 131.0 ns/op 152 B/op 4 allocs/op + // int64s_as_ArrayValue-8 9038806 131.8 ns/op 152 B/op 4 allocs/op + // float64s-8 2022433 597.8 ns/op 344 B/op 12 allocs/op + // float64s_as_ArrayValue-8 1988160 602.1 ns/op 344 B/op 12 allocs/op + // strings-8 8134866 145.5 ns/op 152 B/op 4 allocs/op + // strings_as_ArrayValue-8 8170756 165.6 ns/op 152 B/op 4 allocs/op arrBools := []bool{true, false, true, false, true, false} arrInt64s := []int64{1, 2, 3, 4, 5, 6, 7, 8} @@ -130,10 +130,10 @@ func BenchmarkBools(b *testing.B) { fmt.Println(b7.String()) fmt.Println(b8.String()) - b.ResetTimer() - b.ReportAllocs() - b.Run("bools", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Bools("arr", arrBools) v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) @@ -141,6 +141,9 @@ func BenchmarkBools(b *testing.B) { }) b.Run("bools as ArrayValue", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Array("arr", bools(arrBools)) v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) @@ -148,6 +151,9 @@ func BenchmarkBools(b *testing.B) { }) b.Run("int64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Int64s("arr", arrInt64s) v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) @@ -155,6 +161,9 @@ func BenchmarkBools(b *testing.B) { }) b.Run("int64s as ArrayValue", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Array("arr", int64s(arrInt64s)) v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) @@ -162,6 +171,9 @@ func BenchmarkBools(b *testing.B) { }) b.Run("float64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Float64s("arr", arrFloat64s) v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) @@ -169,6 +181,9 @@ func BenchmarkBools(b *testing.B) { }) b.Run("float64s as ArrayValue", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Array("arr", float64s(arrFloat64s)) v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) @@ -176,6 +191,9 @@ func BenchmarkBools(b *testing.B) { }) b.Run("strings", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Strings("arr", arrStrings) v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) @@ -183,6 +201,9 @@ func BenchmarkBools(b *testing.B) { }) b.Run("strings as ArrayValue", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Array("arr", strings(arrStrings)) v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) diff --git a/log/benchmarks/fields/main.go b/log/benchmarks/fields/main.go new file mode 100644 index 00000000..2a883b79 --- /dev/null +++ b/log/benchmarks/fields/main.go @@ -0,0 +1,19 @@ +/* + * 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 + +func main() {} diff --git a/log/benchmarks/fields/main_test.go b/log/benchmarks/fields/main_test.go new file mode 100644 index 00000000..6f7d8f7b --- /dev/null +++ b/log/benchmarks/fields/main_test.go @@ -0,0 +1,170 @@ +/* + * 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_test + +import ( + "bytes" + "testing" + + "benchmark-fields/encoder" + "benchmark-fields/field-value" + "benchmark-fields/value-interface" + "benchmark-fields/value-struct" +) + +var ( + arrBools = []bool{true, false, true, false, true, false} + arrInt64s = []int64{1, 2, 3, 4, 5, 6, 7, 8} + arrFloat64s = []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8} + arrStrings = []string{"a", "b", "c", "d", "e", "f", "g", "h"} +) + +func BenchmarkFields(b *testing.B) { + + // value_interface/bools-8 10864762 109.4 ns/op 152 B/op 4 allocs/op + // value_struct/bools-8 11207328 107.6 ns/op 152 B/op 4 allocs/op + // field_value/bools-8 11133696 108.8 ns/op 152 B/op 4 allocs/op + + // value_interface/int64s-8 8831475 138.4 ns/op 152 B/op 4 allocs/op + // value_struct/int64s-8 8929010 134.1 ns/op 152 B/op 4 allocs/op + // field_value/int64s-8 8978941 132.0 ns/op 152 B/op 4 allocs/op + + // value_interface/float64s-8 1927635 614.1 ns/op 344 B/op 12 allocs/op + // value_struct/float64s-8 1980488 604.1 ns/op 344 B/op 12 allocs/op + // field_value/float64s-8 1992417 601.2 ns/op 344 B/op 12 allocs/op + + // value_interface/strings-8 8276900 144.9 ns/op 152 B/op 4 allocs/op + // value_struct/strings-8 8107906 148.5 ns/op 152 B/op 4 allocs/op + // field_value/strings-8 8212352 149.6 ns/op 152 B/op 4 allocs/op + + b.Run("value_interface", func(b *testing.B) { + b.Run("bools", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := value_interface.Bools("arr", arrBools) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + b.Run("int64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := value_interface.Int64s("arr", arrInt64s) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + b.Run("float64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := value_interface.Float64s("arr", arrFloat64s) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + b.Run("strings", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := value_interface.Strings("arr", arrStrings) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + }) + + b.Run("value_struct", func(b *testing.B) { + b.Run("bools", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := value_struct.Bools("arr", arrBools) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + b.Run("int64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := value_struct.Int64s("arr", arrInt64s) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + b.Run("float64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := value_struct.Float64s("arr", arrFloat64s) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + b.Run("strings", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := value_struct.Strings("arr", arrStrings) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + }) + + b.Run("field_value", func(b *testing.B) { + b.Run("bools", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := field_value.Bools("arr", arrBools) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + b.Run("int64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := field_value.Int64s("arr", arrInt64s) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + b.Run("float64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := field_value.Float64s("arr", arrFloat64s) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + b.Run("strings", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for b.Loop() { + v := field_value.Strings("arr", arrStrings) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) + } + }) + }) +} diff --git a/log/benchmarks/fields/value-interface/field.go b/log/benchmarks/fields/value-interface/field.go index a358ab74..dd4527c3 100644 --- a/log/benchmarks/fields/value-interface/field.go +++ b/log/benchmarks/fields/value-interface/field.go @@ -16,6 +16,10 @@ package value_interface +import ( + "benchmark-fields/encoder" +) + // Field represents a structured log field with a key and a value. type Field struct { Key string // The name of the field. @@ -109,3 +113,8 @@ func Any(key string, value interface{}) Field { return Reflect(key, val) } } + +func (f *Field) Encode(enc encoder.Encoder) { + enc.AppendKey(f.Key) + f.Val.Encode(enc) +} diff --git a/log/benchmarks/fields/value-interface/field_test.go b/log/benchmarks/fields/value-interface/field_test.go index c30fd891..9f439cb2 100644 --- a/log/benchmarks/fields/value-interface/field_test.go +++ b/log/benchmarks/fields/value-interface/field_test.go @@ -23,46 +23,55 @@ import ( "benchmark-fields/encoder" ) -func BenchmarkBools(b *testing.B) { +func BenchmarkValueInterface(b *testing.B) { - // bools-8 11332849 107.7 ns/op - // int64s-8 9080331 129.8 ns/op - // float64s-8 2051444 583.3 ns/op - // strings-8 8384014 142.4 ns/op + // bools-8 10998112 105.1 ns/op 152 B/op 4 allocs/op + // int64s-8 9017383 131.5 ns/op 152 B/op 4 allocs/op + // float64s-8 1904684 634.8 ns/op 344 B/op 12 allocs/op + // strings-8 8188070 145.1 ns/op 152 B/op 4 allocs/op arrBools := []bool{true, false, true, false, true, false} arrInt64s := []int64{1, 2, 3, 4, 5, 6, 7, 8} arrFloat64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8} arrStrings := []string{"a", "b", "c", "d", "e", "f", "g", "h"} - b.ResetTimer() - b.ReportAllocs() - b.Run("bools", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Bools("arr", arrBools) - WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) } }) b.Run("int64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Int64s("arr", arrInt64s) - WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) } }) b.Run("float64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Float64s("arr", arrFloat64s) - WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) } }) b.Run("strings", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Strings("arr", arrStrings) - WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) } }) } diff --git a/log/benchmarks/fields/value-interface/value.go b/log/benchmarks/fields/value-interface/value.go index 31425c80..cd752f12 100644 --- a/log/benchmarks/fields/value-interface/value.go +++ b/log/benchmarks/fields/value-interface/value.go @@ -133,14 +133,8 @@ type ObjectValue []Field // Encode encodes the data represented by v to an Encoder. func (v ObjectValue) Encode(enc encoder.Encoder) { enc.AppendObjectBegin() - WriteFields(enc, v) - enc.AppendObjectEnd() -} - -// WriteFields writes a slice of Field objects to the encoder. -func WriteFields(enc encoder.Encoder, fields []Field) { - for _, f := range fields { - enc.AppendKey(f.Key) - f.Val.Encode(enc) + for _, f := range v { + f.Encode(enc) } + enc.AppendObjectEnd() } diff --git a/log/benchmarks/fields/value-struct/field.go b/log/benchmarks/fields/value-struct/field.go index e75e78af..d9f2194d 100644 --- a/log/benchmarks/fields/value-struct/field.go +++ b/log/benchmarks/fields/value-struct/field.go @@ -16,6 +16,10 @@ package value_struct +import ( + "benchmark-fields/encoder" +) + // Field represents a structured log field with a key and a value. type Field struct { Key string // The name of the field. @@ -109,3 +113,8 @@ func Any(key string, value interface{}) Field { return Reflect(key, val) } } + +func (f *Field) Encode(enc encoder.Encoder) { + enc.AppendKey(f.Key) + f.Val.Encode(enc) +} diff --git a/log/benchmarks/fields/value-struct/field_test.go b/log/benchmarks/fields/value-struct/field_test.go index 730f413e..0068b01a 100644 --- a/log/benchmarks/fields/value-struct/field_test.go +++ b/log/benchmarks/fields/value-struct/field_test.go @@ -23,46 +23,55 @@ import ( "benchmark-fields/encoder" ) -func BenchmarkBools(b *testing.B) { +func BenchmarkValueStruct(b *testing.B) { - // bools-8 11117685 105.6 ns/op - // int64s-8 9201427 136.9 ns/op - // float64s-8 1878270 618.2 ns/op - // strings-8 8103530 146.1 ns/op + // bools-8 11046146 107.2 ns/op 152 B/op 4 allocs/op + // int64s-8 8929279 131.9 ns/op 152 B/op 4 allocs/op + // float64s-8 2011627 612.2 ns/op 344 B/op 12 allocs/op + // strings-8 7775536 155.2 ns/op 152 B/op 4 allocs/op arrBools := []bool{true, false, true, false, true, false} arrInt64s := []int64{1, 2, 3, 4, 5, 6, 7, 8} arrFloat64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8} arrStrings := []string{"a", "b", "c", "d", "e", "f", "g", "h"} - b.ResetTimer() - b.ReportAllocs() - b.Run("bools", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Bools("arr", arrBools) - WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) } }) b.Run("int64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Int64s("arr", arrInt64s) - WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) } }) b.Run("float64s", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Float64s("arr", arrFloat64s) - WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) } }) b.Run("strings", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { v := Strings("arr", arrStrings) - WriteFields(encoder.NewJSONEncoder(bytes.NewBuffer(nil)), []Field{v}) + v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) } }) } diff --git a/log/benchmarks/fields/value-struct/value.go b/log/benchmarks/fields/value-struct/value.go index 1fc1e709..1eec5540 100644 --- a/log/benchmarks/fields/value-struct/value.go +++ b/log/benchmarks/fields/value-struct/value.go @@ -159,15 +159,10 @@ func ObjectValue(fields ...Field) Value { func (v objVal) Encode(enc encoder.Encoder) { enc.AppendObjectBegin() - WriteFields(enc, v) - enc.AppendObjectEnd() -} - -func WriteFields(enc encoder.Encoder, fields []Field) { - for _, f := range fields { - enc.AppendKey(f.Key) - f.Val.Encode(enc) + for _, f := range v { + f.Encode(enc) } + enc.AppendObjectEnd() } // Encode encodes the Value to the encoder. From 925a1ee996eb22fc1b24c05553c7fe80686b8792 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Mon, 9 Jun 2025 13:19:00 +0800 Subject: [PATCH 09/18] refactor(log): refactoring Field structure --- log/field.go | 342 +++++++++++++++++++++++++++++++++++++++------ log/field_test.go | 14 +- log/field_value.go | 270 ----------------------------------- 3 files changed, 305 insertions(+), 321 deletions(-) delete mode 100644 log/field_value.go diff --git a/log/field.go b/log/field.go index f1257b50..1c229973 100644 --- a/log/field.go +++ b/log/field.go @@ -18,14 +18,32 @@ package log import ( "fmt" + "math" + "unsafe" ) const MsgKey = "msg" +// ValueType represents the type of value stored in a Field. +type ValueType int + +const ( + ValueTypeBool = ValueType(iota) + ValueTypeInt64 + ValueTypeUint64 + ValueTypeFloat64 + ValueTypeString + ValueTypeReflect + ValueTypeArray + ValueTypeObject +) + // 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. + Key string + Type ValueType + Num uint64 + Any any } // Msg creates a string Field with the "msg" key. @@ -38,11 +56,6 @@ 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) @@ -50,7 +63,18 @@ func Nil(key string) Field { // Bool creates a Field for a boolean value. func Bool(key string, val bool) Field { - return Field{Key: key, Val: BoolValue(val)} + if val { + return Field{ + Key: key, + Type: ValueTypeBool, + Num: 1, + } + } + return Field{ + Key: key, + Type: ValueTypeBool, + Num: 0, + } } // BoolPtr creates a Field from a *bool, or a nil Field if the pointer is nil. @@ -63,7 +87,11 @@ func BoolPtr(key string, val *bool) Field { // Int creates a Field for an int value. func Int(key string, val int) Field { - return Field{Key: key, Val: Int64Value(val)} + return Field{ + Key: key, + Type: ValueTypeInt64, + Num: uint64(val), + } } // IntPtr creates a Field from a *int, or returns Nil if pointer is nil. @@ -76,7 +104,11 @@ func IntPtr(key string, val *int) Field { // Int8 creates a Field for an int8 value. func Int8(key string, val int8) Field { - return Field{Key: key, Val: Int64Value(val)} + return Field{ + Key: key, + Type: ValueTypeInt64, + Num: uint64(val), + } } // Int8Ptr creates a Field from a *int8, or returns Nil if pointer is nil. @@ -89,7 +121,11 @@ func Int8Ptr(key string, val *int8) Field { // Int16 creates a Field for an int16 value. func Int16(key string, val int16) Field { - return Field{Key: key, Val: Int64Value(val)} + return Field{ + Key: key, + Type: ValueTypeInt64, + Num: uint64(val), + } } // Int16Ptr creates a Field from a *int16, or returns Nil if pointer is nil. @@ -102,7 +138,11 @@ func Int16Ptr(key string, val *int16) Field { // Int32 creates a Field for an int32 value. func Int32(key string, val int32) Field { - return Field{Key: key, Val: Int64Value(val)} + return Field{ + Key: key, + Type: ValueTypeInt64, + Num: uint64(val), + } } // Int32Ptr creates a Field from a *int32, or returns Nil if pointer is nil. @@ -115,7 +155,11 @@ func Int32Ptr(key string, val *int32) Field { // Int64 creates a Field for an int64 value. func Int64(key string, val int64) Field { - return Field{Key: key, Val: Int64Value(val)} + return Field{ + Key: key, + Type: ValueTypeInt64, + Num: uint64(val), + } } // Int64Ptr creates a Field from a *int64, or returns Nil if pointer is nil. @@ -128,7 +172,11 @@ func Int64Ptr(key string, val *int64) Field { // Uint creates a Field for an uint value. func Uint(key string, val uint) Field { - return Field{Key: key, Val: Uint64Value(val)} + return Field{ + Key: key, + Type: ValueTypeUint64, + Num: uint64(val), + } } // UintPtr creates a Field from a *uint, or returns Nil if pointer is nil. @@ -141,7 +189,11 @@ func UintPtr(key string, val *uint) Field { // Uint8 creates a Field for an uint8 value. func Uint8(key string, val uint8) Field { - return Field{Key: key, Val: Uint64Value(val)} + return Field{ + Key: key, + Type: ValueTypeUint64, + Num: uint64(val), + } } // Uint8Ptr creates a Field from a *uint8, or returns Nil if pointer is nil. @@ -154,7 +206,11 @@ func Uint8Ptr(key string, val *uint8) Field { // Uint16 creates a Field for an uint16 value. func Uint16(key string, val uint16) Field { - return Field{Key: key, Val: Uint64Value(val)} + return Field{ + Key: key, + Type: ValueTypeUint64, + Num: uint64(val), + } } // Uint16Ptr creates a Field from a *uint16, or returns Nil if pointer is nil. @@ -167,7 +223,11 @@ func Uint16Ptr(key string, val *uint16) Field { // Uint32 creates a Field for an uint32 value. func Uint32(key string, val uint32) Field { - return Field{Key: key, Val: Uint64Value(val)} + return Field{ + Key: key, + Type: ValueTypeUint64, + Num: uint64(val), + } } // Uint32Ptr creates a Field from a *uint32, or returns Nil if pointer is nil. @@ -180,7 +240,11 @@ func Uint32Ptr(key string, val *uint32) Field { // Uint64 creates a Field for an uint64 value. func Uint64(key string, val uint64) Field { - return Field{Key: key, Val: Uint64Value(val)} + return Field{ + Key: key, + Type: ValueTypeUint64, + Num: val, + } } // Uint64Ptr creates a Field from a *uint64, or returns Nil if pointer is nil. @@ -193,7 +257,11 @@ func Uint64Ptr(key string, val *uint64) Field { // Float32 creates a Field for a float32 value. func Float32(key string, val float32) Field { - return Field{Key: key, Val: Float64Value(val)} + return Field{ + Key: key, + Type: ValueTypeFloat64, + Num: math.Float64bits(float64(val)), + } } // Float32Ptr creates a Field from a *float32, or returns Nil if pointer is nil. @@ -206,7 +274,11 @@ func Float32Ptr(key string, val *float32) Field { // Float64 creates a Field for a float64 value. func Float64(key string, val float64) Field { - return Field{Key: key, Val: Float64Value(val)} + return Field{ + Key: key, + Type: ValueTypeFloat64, + Num: math.Float64bits(val), + } } // Float64Ptr creates a Field from a *float64, or returns Nil if pointer is nil. @@ -219,7 +291,12 @@ func Float64Ptr(key string, val *float64) Field { // String creates a Field for a string value. func String(key string, val string) Field { - return Field{Key: key, Val: StringValue(val)} + return Field{ + Key: key, + Type: ValueTypeString, + Num: uint64(len(val)), // Store the length of the string + Any: unsafe.StringData(val), // Store the pointer to string data + } } // StringPtr creates a Field from a *string, or returns Nil if pointer is nil. @@ -230,84 +307,232 @@ func StringPtr(key string, val *string) Field { 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)} +// Reflect wraps any value into a Field using reflection. +func Reflect(key string, val interface{}) Field { + return Field{ + Key: key, + Type: ValueTypeReflect, + Any: 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)} +type bools []bool + +// EncodeArray encodes a slice of bools using the Encoder interface. +func (arr bools) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendBool(v) + } } // Bools creates a Field with a slice of booleans. func Bools(key string, val []bool) Field { - return Field{Key: key, Val: BoolsValue(val)} + return Array(key, bools(val)) +} + +type ints []int + +// EncodeArray encodes a slice of ints using the Encoder interface. +func (arr ints) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendInt64(int64(v)) + } } // Ints creates a Field with a slice of integers. func Ints(key string, val []int) Field { - return Field{Key: key, Val: IntsValue(val)} + return Array(key, ints(val)) +} + +type int8s []int8 + +// EncodeArray encodes a slice of int8s using the Encoder interface. +func (arr int8s) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendInt64(int64(v)) + } } // Int8s creates a Field with a slice of int8 values. func Int8s(key string, val []int8) Field { - return Field{Key: key, Val: Int8sValue(val)} + return Array(key, int8s(val)) +} + +type int16s []int16 + +// EncodeArray encodes a slice of int16s using the Encoder interface. +func (arr int16s) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendInt64(int64(v)) + } } // Int16s creates a Field with a slice of int16 values. func Int16s(key string, val []int16) Field { - return Field{Key: key, Val: Int16sValue(val)} + return Array(key, int16s(val)) +} + +type int32s []int32 + +// EncodeArray encodes a slice of int32s using the Encoder interface. +func (arr int32s) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendInt64(int64(v)) + } } // Int32s creates a Field with a slice of int32 values. func Int32s(key string, val []int32) Field { - return Field{Key: key, Val: Int32sValue(val)} + return Array(key, int32s(val)) +} + +type int64s []int64 + +// EncodeArray encodes a slice of int64s using the Encoder interface. +func (arr int64s) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendInt64(v) + } } // Int64s creates a Field with a slice of int64 values. func Int64s(key string, val []int64) Field { - return Field{Key: key, Val: Int64sValue(val)} + return Array(key, int64s(val)) +} + +type uints []uint + +// EncodeArray encodes a slice of uints using the Encoder interface. +func (arr uints) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendUint64(uint64(v)) + } } // Uints creates a Field with a slice of unsigned integers. func Uints(key string, val []uint) Field { - return Field{Key: key, Val: UintsValue(val)} + return Array(key, uints(val)) +} + +type uint8s []uint8 + +// EncodeArray encodes a slice of uint8s using the Encoder interface. +func (arr uint8s) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendUint64(uint64(v)) + } } // Uint8s creates a Field with a slice of uint8 values. func Uint8s(key string, val []uint8) Field { - return Field{Key: key, Val: Uint8sValue(val)} + return Array(key, uint8s(val)) +} + +type uint16s []uint16 + +// EncodeArray encodes a slice of uint16s using the Encoder interface. +func (arr uint16s) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendUint64(uint64(v)) + } } // Uint16s creates a Field with a slice of uint16 values. func Uint16s(key string, val []uint16) Field { - return Field{Key: key, Val: Uint16sValue(val)} + return Array(key, uint16s(val)) +} + +type uint32s []uint32 + +// EncodeArray encodes a slice of uint32s using the Encoder interface. +func (arr uint32s) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendUint64(uint64(v)) + } } // Uint32s creates a Field with a slice of uint32 values. func Uint32s(key string, val []uint32) Field { - return Field{Key: key, Val: Uint32sValue(val)} + return Array(key, uint32s(val)) +} + +type uint64s []uint64 + +// EncodeArray encodes a slice of uint64s using the Encoder interface. +func (arr uint64s) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendUint64(v) + } } // Uint64s creates a Field with a slice of uint64 values. func Uint64s(key string, val []uint64) Field { - return Field{Key: key, Val: Uint64sValue(val)} + return Array(key, uint64s(val)) +} + +type float32s []float32 + +// EncodeArray encodes a slice of float32s using the Encoder interface. +func (arr float32s) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendFloat64(float64(v)) + } } // Float32s creates a Field with a slice of float32 values. func Float32s(key string, val []float32) Field { - return Field{Key: key, Val: Float32sValue(val)} + return Array(key, float32s(val)) +} + +type float64s []float64 + +// EncodeArray encodes a slice of float64s using the Encoder interface. +func (arr float64s) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendFloat64(v) + } } // Float64s creates a Field with a slice of float64 values. func Float64s(key string, val []float64) Field { - return Field{Key: key, Val: Float64sValue(val)} + return Array(key, float64s(val)) +} + +type sliceOfString []string + +// EncodeArray encodes a slice of strings using the Encoder interface. +func (arr sliceOfString) EncodeArray(enc Encoder) { + for _, v := range arr { + enc.AppendString(v) + } } // Strings creates a Field with a slice of strings. func Strings(key string, val []string) Field { - return Field{Key: key, Val: StringsValue(val)} + return Array(key, sliceOfString(val)) +} + +// ArrayValue is an interface for types that can be encoded as array. +type ArrayValue interface { + EncodeArray(enc Encoder) +} + +// Array creates a Field with array type, using the ArrayValue interface. +func Array(key string, val ArrayValue) Field { + return Field{ + Key: key, + Type: ValueTypeArray, + Any: 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, + Type: ValueTypeObject, + Any: fields, + } } // Any creates a Field from a value of any type by inspecting its dynamic type. @@ -420,3 +645,38 @@ func Any(key string, value interface{}) Field { return Reflect(key, val) } } + +// Encode encodes the Field into the Encoder based on its type. +func (f *Field) Encode(enc Encoder) { + enc.AppendKey(f.Key) + switch f.Type { + case ValueTypeBool: + enc.AppendBool(f.Num != 0) + case ValueTypeInt64: + enc.AppendInt64(int64(f.Num)) + case ValueTypeUint64: + enc.AppendUint64(f.Num) + case ValueTypeFloat64: + enc.AppendFloat64(math.Float64frombits(f.Num)) + case ValueTypeString: + enc.AppendString(unsafe.String(f.Any.(*byte), f.Num)) + case ValueTypeReflect: + enc.AppendReflect(f.Any) + case ValueTypeArray: + enc.AppendArrayBegin() + f.Any.(ArrayValue).EncodeArray(enc) + enc.AppendArrayEnd() + case ValueTypeObject: + enc.AppendObjectBegin() + WriteFields(enc, f.Any.([]Field)) + enc.AppendObjectEnd() + default: // for linter + } +} + +// WriteFields writes a slice of Field objects to the encoder. +func WriteFields(enc Encoder, fields []Field) { + for _, f := range fields { + f.Encode(enc) + } +} diff --git a/log/field_test.go b/log/field_test.go index a260c6a0..d31e4b01 100644 --- a/log/field_test.go +++ b/log/field_test.go @@ -32,7 +32,7 @@ var testFields = []Field{ Msg("hello world\n\\\t\"\r"), Any("null", nil), Any("bool", false), - Any("bool_ptr", ptr(false)), + Any("bool_ptr", ptr(true)), Any("bool_ptr_nil", (*bool)(nil)), Any("bools", []bool{true, true, false}), Any("int", int(1)), @@ -87,7 +87,6 @@ var testFields = []Field{ 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}), } @@ -114,7 +113,7 @@ func TestJSONEncoder(t *testing.T) { "msg": "hello world\n\\\t\"\r", "null": null, "bool": false, - "bool_ptr": false, + "bool_ptr": true, "bool_ptr_nil": null, "bools": [ true, @@ -225,11 +224,6 @@ func TestJSONEncoder(t *testing.T) { "b", "c" ], - "array": [ - 1, - 2, - "a" - ], "object": { "int64": 1, "uint64": 1, @@ -275,7 +269,7 @@ func TestTextEncoder(t *testing.T) { } 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]||` + + `bool=false||bool_ptr=true||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]||` + @@ -289,7 +283,7 @@ func TestTextEncoder(t *testing.T) { `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={"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) - } -} From 7b4d2b2456205aee86c99d61d96e2e3baff83f98 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Mon, 9 Jun 2025 17:47:21 +0800 Subject: [PATCH 10/18] feat(log): add Fields function --- doc/examples/servers/gin/server.go | 37 +++++--- gs/http.go | 32 +++++-- log/field_encoder.go | 140 +++++++++++++++-------------- log/field_test.go | 4 +- log/log.go | 26 ++++-- log/log_tag.go | 32 +++++++ log/log_tag_test.go | 52 +++++++++++ log/log_test.go | 5 ++ 8 files changed, 231 insertions(+), 97 deletions(-) create mode 100644 log/log_tag_test.go diff --git a/doc/examples/servers/gin/server.go b/doc/examples/servers/gin/server.go index 8dc9431e..70a49010 100644 --- a/doc/examples/servers/gin/server.go +++ b/doc/examples/servers/gin/server.go @@ -27,25 +27,38 @@ import ( ) func init() { - gs.Provide(NewSimpleGinServer, gs.TagArg(""), gs.TagArg("${http.server}")).AsServer() -} - -type HttpServerConfig struct { - Address string `value:"${addr:=0.0.0.0:9090}"` - ReadTimeout time.Duration `value:"${readTimeout:=5s}"` - WriteTimeout time.Duration `value:"${writeTimeout:=5s}"` + 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, cfg HttpServerConfig) *SimpleGinServer { +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{ - Addr: cfg.Address, - Handler: e, - ReadTimeout: cfg.ReadTimeout, - WriteTimeout: cfg.WriteTimeout, + Handler: e, + Addr: arg.Address, + ReadTimeout: arg.ReadTimeout, + ReadHeaderTimeout: arg.HeaderTimeout, + WriteTimeout: arg.WriteTimeout, + IdleTimeout: arg.IdleTimeout, }} } 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/log/field_encoder.go b/log/field_encoder.go index ce60d9a3..1282cc2e 100644 --- a/log/field_encoder.go +++ b/log/field_encoder.go @@ -123,7 +123,7 @@ func (enc *JSONEncoder) AppendKey(key string) { enc.appendSeparator() enc.last = jsonTokenKey enc.buf.WriteByte('"') - enc.safeAddString(key) + SafeWriteString(enc.buf, key) enc.buf.WriteByte('"') enc.buf.WriteByte(':') } @@ -161,7 +161,7 @@ func (enc *JSONEncoder) AppendString(v string) { enc.appendSeparator() enc.last = jsonTokenValue enc.buf.WriteByte('"') - enc.safeAddString(v) + SafeWriteString(enc.buf, v) enc.buf.WriteByte('"') } @@ -172,76 +172,13 @@ func (enc *JSONEncoder) AppendReflect(v interface{}) { b, err := json.Marshal(v) if err != nil { enc.buf.WriteByte('"') - enc.safeAddString(err.Error()) + SafeWriteString(enc.buf, 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 { @@ -314,7 +251,7 @@ func (enc *TextEncoder) AppendKey(key string) { } else { enc.firstField = true } - enc.buf.WriteString(key) + SafeWriteString(enc.buf, key) enc.buf.WriteByte('=') } @@ -360,7 +297,7 @@ func (enc *TextEncoder) AppendString(v string) { enc.jsonEncoder.AppendString(v) return } - enc.buf.WriteString(v) + SafeWriteString(enc.buf, v) } // AppendReflect uses reflection to marshal any value as JSON. @@ -372,8 +309,73 @@ func (enc *TextEncoder) AppendReflect(v interface{}) { } b, err := json.Marshal(v) if err != nil { - enc.AppendString(err.Error()) + SafeWriteString(enc.buf, err.Error()) return } enc.buf.Write(b) } + +/************************************* string ********************************/ + +// SafeWriteString escapes and writes a string according to JSON rules. +func SafeWriteString(buf *bytes.Buffer, s string) { + for i := 0; i < len(s); { + // Try to add a single-byte (ASCII) character directly + if tryAddRuneSelf(buf, s[i]) { + i++ + continue + } + // Decode multi-byte UTF-8 character + r, size := utf8.DecodeRuneInString(s[i:]) + // Handle invalid UTF-8 encoding + if tryAddRuneError(buf, r, size) { + i++ + continue + } + // Valid multi-byte rune; add as is + buf.WriteString(s[i : i+size]) + i += size + } +} + +// tryAddRuneSelf handles ASCII characters and escapes control/quote characters. +func tryAddRuneSelf(buf *bytes.Buffer, b byte) bool { + const _hex = "0123456789abcdef" + if b >= utf8.RuneSelf { + return false // not a single-byte rune + } + if 0x20 <= b && b != '\\' && b != '"' { + buf.WriteByte(b) + return true + } + // Handle escaping + switch b { + case '\\', '"': + buf.WriteByte('\\') + buf.WriteByte(b) + case '\n': + buf.WriteByte('\\') + buf.WriteByte('n') + case '\r': + buf.WriteByte('\\') + buf.WriteByte('r') + case '\t': + buf.WriteByte('\\') + buf.WriteByte('t') + default: + // Encode bytes < 0x20, except for the escape sequences above. + buf.WriteString(`\u00`) + buf.WriteByte(_hex[b>>4]) + buf.WriteByte(_hex[b&0xF]) + } + return true +} + +// tryAddRuneError checks and escapes invalid UTF-8 runes. +func tryAddRuneError(buf *bytes.Buffer, r rune, size int) bool { + if r == utf8.RuneError && size == 1 { + buf.WriteString(`\ufffd`) + return true + } + return false +} diff --git a/log/field_test.go b/log/field_test.go index d31e4b01..9e8d77a4 100644 --- a/log/field_test.go +++ b/log/field_test.go @@ -268,7 +268,7 @@ func TestTextEncoder(t *testing.T) { enc.AppendArrayEnd() } enc.AppendEncoderEnd() - const expect = "msg=hello 中国||msg=hello world\n\\\t\"\r||null=null||" + + const expect = `msg=hello 中国||msg=hello world\n\\\t\"\r||null=null||` + `bool=false||bool_ptr=true||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]||` + @@ -282,7 +282,7 @@ func TestTextEncoder(t *testing.T) { `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"]||` + + `string=\ufffd\ufffd\ufffd\ufffd\u0008||string_ptr=a||string_ptr_nil=null||string_slice=["a","b","c"]||` + `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/log.go b/log/log.go index a8098512..33297195 100644 --- a/log/log.go +++ b/log/log.go @@ -21,6 +21,7 @@ import ( "time" "github.com/go-spring/spring-core/log/internal" + "github.com/go-spring/spring-core/util" ) // TimeNow is a function that can be overridden to provide custom timestamp behavior (e.g., for testing). @@ -32,49 +33,58 @@ 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 +// Fields converts a map of string keys to any values to a slice of Field. +func Fields(fields map[string]any) []Field { + var ret []Field + for _, k := range util.OrderedMapKeys(fields) { + ret = append(ret, Any(k, fields[k])) + } + return ret +} + // 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()...) + 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()...) + 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...) + 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...) + 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...) + 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...) + 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(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) { +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 diff --git a/log/log_tag.go b/log/log_tag.go index cac77019..8223d4bf 100644 --- a/log/log_tag.go +++ b/log/log_tag.go @@ -17,6 +17,7 @@ package log import ( + "strings" "sync/atomic" ) @@ -68,9 +69,40 @@ func (m *Tag) SetLogger(logger *Logger) { m.v.Store(logger) } +// isValidTag checks whether the tag is valid according to the following rules: +// 1. The length must be between 4 and 36 characters. +// 2. Only lowercase letters (a-z), digits (0-9), and underscores (_) are allowed. +// 3. The tag can start with an underscore. +// 4. Underscores separate the tag into 1 to 4 non-empty segments. +// 5. No empty segments are allowed (i.e., no consecutive or trailing underscores). +func isValidTag(tag string) bool { + if len(tag) < 4 || len(tag) > 36 { + return false + } + for i := 0; i < len(tag); i++ { + c := tag[i] + if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '_' { + return false + } + } + ss := strings.Split(strings.TrimPrefix(tag, "_"), "_") + if len(ss) < 1 || len(ss) > 4 { + return false + } + for _, s := range ss { + if s == "" { + return false + } + } + return true +} + // 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 { + if !isValidTag(tag) { + panic("invalid tag name") + } m, ok := tagMap[tag] if !ok { m = &Tag{s: tag} diff --git a/log/log_tag_test.go b/log/log_tag_test.go new file mode 100644 index 00000000..5ec75e47 --- /dev/null +++ b/log/log_tag_test.go @@ -0,0 +1,52 @@ +/* + * 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 ( + "strings" + "testing" +) + +func TestIsValidTag(t *testing.T) { + tests := []struct { + name string + tag string + want bool + }{ + {"valid_1segments", "_def", true}, + {"valid_4segments", "service_module_submodule_component00", true}, + {"too_short_3", "abc", false}, + {"too_long_37", strings.Repeat("a", 37), false}, + {"uppercase", "Invalid_Tag", false}, + {"special_char", "tag!name", false}, + {"space", "tag name", false}, + {"hyphen", "tag-name", false}, + {"too_many_segments", "a_b_c_d_e", false}, + {"leading_underscore_1", "_service_component", true}, + {"leading_underscore_2", "__service_component", false}, + {"trailing_underscore_1", "service_component_", false}, + {"trailing_underscore_2", "service_component__", false}, + {"consecutive_underscore", "service__component", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isValidTag(tt.tag); got != tt.want { + t.Errorf("isValidTag(%q) = %v, want %v", tt.tag, got, tt.want) + } + }) + } +} diff --git a/log/log_test.go b/log/log_test.go index fca5a7b7..a1b812f2 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -89,4 +89,9 @@ func TestLog(t *testing.T) { log.Warn(ctx, TagDefault, log.Msgf("hello %s", "world")) log.Error(ctx, TagDefault, log.Msgf("hello %s", "world")) log.Panic(ctx, TagDefault, log.Msgf("hello %s", "world")) + + log.Error(ctx, TagDefault, log.Fields(map[string]any{ + "key1": "value1", + "key2": "value2", + })...) } From 1a39b3e99810d566fd83ffff1d0d6d7c64fc8b9e Mon Sep 17 00:00:00 2001 From: lvan100 Date: Mon, 9 Jun 2025 20:15:54 +0800 Subject: [PATCH 11/18] feat(log): add formatted logging function --- log/log.go | 55 ++++++++++++++++++++++++++++++++++++++++++------- log/log_test.go | 6 ++++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/log/log.go b/log/log.go index 33297195..50d97d89 100644 --- a/log/log.go +++ b/log/log.go @@ -45,46 +45,85 @@ func Fields(fields map[string]any) []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()) + Record(ctx, TraceLevel, tag, fn()...) + } +} + +// Tracef logs a message at TraceLevel using a tag and a formatted message. +func Tracef(ctx context.Context, tag *Tag, format string, args ...any) { + if tag.GetLogger().EnableLevel(TraceLevel) { + Record(ctx, TraceLevel, tag, Msgf(format, args...)) } } // 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()) + Record(ctx, DebugLevel, tag, fn()...) + } +} + +// Debugf logs a message at DebugLevel using a tag and a formatted message. +func Debugf(ctx context.Context, tag *Tag, format string, args ...any) { + if tag.GetLogger().EnableLevel(DebugLevel) { + Record(ctx, DebugLevel, tag, Msgf(format, args...)) } } // Info logs a message at InfoLevel using structured fields. func Info(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, InfoLevel, tag, fields) + Record(ctx, InfoLevel, tag, fields...) +} + +// Infof logs a message at InfoLevel using a formatted message. +func Infof(ctx context.Context, tag *Tag, format string, args ...any) { + Record(ctx, InfoLevel, tag, Msgf(format, args...)) } // Warn logs a message at WarnLevel using structured fields. func Warn(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, WarnLevel, tag, fields) + Record(ctx, WarnLevel, tag, fields...) +} + +// Warnf logs a message at WarnLevel using a formatted message. +func Warnf(ctx context.Context, tag *Tag, format string, args ...any) { + Record(ctx, WarnLevel, tag, Msgf(format, args...)) } // Error logs a message at ErrorLevel using structured fields. func Error(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, ErrorLevel, tag, fields) + Record(ctx, ErrorLevel, tag, fields...) +} + +// Errorf logs a message at ErrorLevel using a formatted message. +func Errorf(ctx context.Context, tag *Tag, format string, args ...any) { + Record(ctx, ErrorLevel, tag, Msgf(format, args...)) } // Panic logs a message at PanicLevel using structured fields. func Panic(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, PanicLevel, tag, fields) + Record(ctx, PanicLevel, tag, fields...) +} + +// Panicf logs a message at PanicLevel using a formatted message. +func Panicf(ctx context.Context, tag *Tag, format string, args ...any) { + Record(ctx, PanicLevel, tag, Msgf(format, args...)) } // Fatal logs a message at FatalLevel using structured fields. func Fatal(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, FatalLevel, tag, fields) + Record(ctx, FatalLevel, tag, fields...) +} + +// Fatalf logs a message at FatalLevel using a formatted message. +func Fatalf(ctx context.Context, tag *Tag, format string, args ...any) { + Record(ctx, FatalLevel, tag, Msgf(format, args...)) } // 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) { +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 diff --git a/log/log_test.go b/log/log_test.go index a1b812f2..b49ab5b7 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -85,6 +85,12 @@ func TestLog(t *testing.T) { log.Panic(ctx, TagRequestIn, log.Msgf("hello %s", "world")) log.Fatal(ctx, TagRequestIn, log.Msgf("hello %s", "world")) + log.Infof(ctx, TagRequestIn, "hello %s", "world") + log.Warnf(ctx, TagRequestIn, "hello %s", "world") + log.Errorf(ctx, TagRequestIn, "hello %s", "world") + log.Panicf(ctx, TagRequestIn, "hello %s", "world") + log.Fatalf(ctx, TagRequestIn, "hello %s", "world") + log.Info(ctx, TagDefault, log.Msgf("hello %s", "world")) log.Warn(ctx, TagDefault, log.Msgf("hello %s", "world")) log.Error(ctx, TagDefault, log.Msgf("hello %s", "world")) From c52349acd75cf7587021d58a3c0c3cea15de3b9e Mon Sep 17 00:00:00 2001 From: lvan100 Date: Tue, 10 Jun 2025 13:30:23 +0800 Subject: [PATCH 12/18] refactor(log): refactoring the log module --- log/benchmarks/fields/encoder/encoder.go | 5 - log/benchmarks/fields/field-value/field.go | 10 +- .../fields/field-value/field_test.go | 2 +- log/benchmarks/fields/main_test.go | 4 +- log/benchmarks/fields/value-struct/value.go | 42 +- log/field.go | 385 +++--------------- log/field_encoder.go | 22 +- log/field_test.go | 41 +- log/log.go | 42 +- log/log_event.go | 4 + log/log_level.go | 10 +- log/log_reader.go | 2 +- log/plugin_logger.go | 4 +- log/plugin_logger_test.go | 2 +- util/type.go | 15 + util/value.go | 5 + 16 files changed, 164 insertions(+), 431 deletions(-) diff --git a/log/benchmarks/fields/encoder/encoder.go b/log/benchmarks/fields/encoder/encoder.go index 280068cf..d4ee2890 100644 --- a/log/benchmarks/fields/encoder/encoder.go +++ b/log/benchmarks/fields/encoder/encoder.go @@ -65,11 +65,6 @@ func NewJSONEncoder(buf *bytes.Buffer) *JSONEncoder { } } -// 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() diff --git a/log/benchmarks/fields/field-value/field.go b/log/benchmarks/fields/field-value/field.go index ceae4259..481fff0a 100644 --- a/log/benchmarks/fields/field-value/field.go +++ b/log/benchmarks/fields/field-value/field.go @@ -40,11 +40,11 @@ const ( // Field represents a structured log field with a key and it's value. type Field struct { - Key string // field key - Type ValueType // value type - Num uint64 // numeric value - Str string // string value - Any any // other value + Key string + Type ValueType + Num uint64 + Str string + Any any } // Nil creates a Field with a nil value. diff --git a/log/benchmarks/fields/field-value/field_test.go b/log/benchmarks/fields/field-value/field_test.go index f06ddd1e..3bcb32b8 100644 --- a/log/benchmarks/fields/field-value/field_test.go +++ b/log/benchmarks/fields/field-value/field_test.go @@ -58,7 +58,7 @@ func (arr strings) EncodeArray(enc encoder.Encoder) { func BenchmarkFieldValue(b *testing.B) { - // bools-8 10706354 123.8 ns/op 152 B/op 4 allocs/op + // bools-8 10706354 123.8 ns/op 152 B/op 4 allocs/op // bools_as_ArrayValue-8 11192071 107.9 ns/op 152 B/op 4 allocs/op // int64s-8 9132631 131.0 ns/op 152 B/op 4 allocs/op // int64s_as_ArrayValue-8 9038806 131.8 ns/op 152 B/op 4 allocs/op diff --git a/log/benchmarks/fields/main_test.go b/log/benchmarks/fields/main_test.go index 6f7d8f7b..5f8ba54d 100644 --- a/log/benchmarks/fields/main_test.go +++ b/log/benchmarks/fields/main_test.go @@ -35,11 +35,11 @@ var ( func BenchmarkFields(b *testing.B) { - // value_interface/bools-8 10864762 109.4 ns/op 152 B/op 4 allocs/op + // value_interface/bools-8 10864762 109.4 ns/op 152 B/op 4 allocs/op // value_struct/bools-8 11207328 107.6 ns/op 152 B/op 4 allocs/op // field_value/bools-8 11133696 108.8 ns/op 152 B/op 4 allocs/op - // value_interface/int64s-8 8831475 138.4 ns/op 152 B/op 4 allocs/op + // value_interface/int64s-8 8831475 138.4 ns/op 152 B/op 4 allocs/op // value_struct/int64s-8 8929010 134.1 ns/op 152 B/op 4 allocs/op // field_value/int64s-8 8978941 132.0 ns/op 152 B/op 4 allocs/op diff --git a/log/benchmarks/fields/value-struct/value.go b/log/benchmarks/fields/value-struct/value.go index 1eec5540..e607d1a4 100644 --- a/log/benchmarks/fields/value-struct/value.go +++ b/log/benchmarks/fields/value-struct/value.go @@ -23,10 +23,6 @@ import ( "benchmark-fields/encoder" ) -type ( - StringDataPtr *byte // used in Value.Any when the Value is a string -) - type ValueType int const ( @@ -85,7 +81,7 @@ func StringValue(v string) Value { return Value{ Type: ValueTypeString, Num: uint64(len(v)), - Any: StringDataPtr(unsafe.StringData(v)), + Any: unsafe.StringData(v), } } @@ -131,14 +127,6 @@ func StringsValue(v []string) Value { type arrVal []Value -// ArrayValue represents a slice of Value carried by Field. -func ArrayValue(val ...Value) Value { - return Value{ - Type: ValueTypeArray, - Any: arrVal(val), - } -} - func (v arrVal) Encode(enc encoder.Encoder) { enc.AppendArrayBegin() for _, val := range v { @@ -147,16 +135,16 @@ func (v arrVal) Encode(enc encoder.Encoder) { enc.AppendArrayEnd() } -type objVal []Field - -// ObjectValue represents a slice of Field carried by Field. -func ObjectValue(fields ...Field) Value { +// ArrayValue represents a slice of Value carried by Field. +func ArrayValue(val ...Value) Value { return Value{ - Type: ValueTypeObject, - Any: objVal(fields), + Type: ValueTypeArray, + Any: arrVal(val), } } +type objVal []Field + func (v objVal) Encode(enc encoder.Encoder) { enc.AppendObjectBegin() for _, f := range v { @@ -165,21 +153,25 @@ func (v objVal) Encode(enc encoder.Encoder) { enc.AppendObjectEnd() } +// ObjectValue represents a slice of Field carried by Field. +func ObjectValue(fields ...Field) Value { + return Value{ + Type: ValueTypeObject, + Any: objVal(fields), + } +} + // Encode encodes the Value to the encoder. func (v Value) Encode(enc encoder.Encoder) { switch v.Type { case ValueTypeBool: - if v.Num == 0 { - enc.AppendBool(false) - } else { - enc.AppendBool(true) - } + enc.AppendBool(v.Num != 0) case ValueTypeInt64: enc.AppendInt64(int64(v.Num)) case ValueTypeFloat64: enc.AppendFloat64(math.Float64frombits(v.Num)) case ValueTypeString: - enc.AppendString(unsafe.String(v.Any.(StringDataPtr), v.Num)) + enc.AppendString(unsafe.String(v.Any.(*byte), v.Num)) case ValueTypeReflect: enc.AppendReflect(v.Any) case ValueTypeBools: diff --git a/log/field.go b/log/field.go index 1c229973..fa7e9c91 100644 --- a/log/field.go +++ b/log/field.go @@ -20,6 +20,8 @@ import ( "fmt" "math" "unsafe" + + "github.com/go-spring/spring-core/util" ) const MsgKey = "msg" @@ -86,7 +88,7 @@ func BoolPtr(key string, val *bool) Field { } // Int creates a Field for an int value. -func Int(key string, val int) Field { +func Int[T util.IntType](key string, val T) Field { return Field{ Key: key, Type: ValueTypeInt64, @@ -95,83 +97,15 @@ func Int(key string, val int) Field { } // IntPtr creates a Field from a *int, or returns Nil if pointer is nil. -func IntPtr(key string, val *int) Field { +func IntPtr[T util.IntType](key string, val *T) 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, - Type: ValueTypeInt64, - Num: uint64(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, - Type: ValueTypeInt64, - Num: uint64(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, - Type: ValueTypeInt64, - Num: uint64(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, - Type: ValueTypeInt64, - Num: uint64(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 { +func Uint[T util.UintType](key string, val T) Field { return Field{ Key: key, Type: ValueTypeUint64, @@ -180,83 +114,15 @@ func Uint(key string, val uint) Field { } // UintPtr creates a Field from a *uint, or returns Nil if pointer is nil. -func UintPtr(key string, val *uint) Field { +func UintPtr[T util.UintType](key string, val *T) 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, - Type: ValueTypeUint64, - Num: uint64(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, - Type: ValueTypeUint64, - Num: uint64(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, - Type: ValueTypeUint64, - Num: uint64(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, - Type: ValueTypeUint64, - Num: 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 { +// Float creates a Field for a float32 value. +func Float[T util.FloatType](key string, val T) Field { return Field{ Key: key, Type: ValueTypeFloat64, @@ -264,29 +130,12 @@ func Float32(key string, val float32) Field { } } -// 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, - Type: ValueTypeFloat64, - Num: math.Float64bits(val), - } -} - -// Float64Ptr creates a Field from a *float64, or returns Nil if pointer is nil. -func Float64Ptr(key string, val *float64) Field { +// FloatPtr creates a Field from a *float32, or returns Nil if pointer is nil. +func FloatPtr[T util.FloatType](key string, val *T) Field { if val == nil { return Nil(key) } - return Float64(key, *val) + return Float(key, *val) } // String creates a Field for a string value. @@ -330,172 +179,46 @@ func Bools(key string, val []bool) Field { return Array(key, bools(val)) } -type ints []int +type sliceOfInt[T util.IntType] []T // EncodeArray encodes a slice of ints using the Encoder interface. -func (arr ints) EncodeArray(enc Encoder) { +func (arr sliceOfInt[T]) EncodeArray(enc Encoder) { for _, v := range arr { enc.AppendInt64(int64(v)) } } // Ints creates a Field with a slice of integers. -func Ints(key string, val []int) Field { - return Array(key, ints(val)) -} - -type int8s []int8 - -// EncodeArray encodes a slice of int8s using the Encoder interface. -func (arr int8s) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendInt64(int64(v)) - } -} - -// Int8s creates a Field with a slice of int8 values. -func Int8s(key string, val []int8) Field { - return Array(key, int8s(val)) -} - -type int16s []int16 - -// EncodeArray encodes a slice of int16s using the Encoder interface. -func (arr int16s) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendInt64(int64(v)) - } -} - -// Int16s creates a Field with a slice of int16 values. -func Int16s(key string, val []int16) Field { - return Array(key, int16s(val)) -} - -type int32s []int32 - -// EncodeArray encodes a slice of int32s using the Encoder interface. -func (arr int32s) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendInt64(int64(v)) - } -} - -// Int32s creates a Field with a slice of int32 values. -func Int32s(key string, val []int32) Field { - return Array(key, int32s(val)) -} - -type int64s []int64 - -// EncodeArray encodes a slice of int64s using the Encoder interface. -func (arr int64s) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendInt64(v) - } -} - -// Int64s creates a Field with a slice of int64 values. -func Int64s(key string, val []int64) Field { - return Array(key, int64s(val)) +func Ints[T util.IntType](key string, val []T) Field { + return Array(key, sliceOfInt[T](val)) } -type uints []uint +type sliceOfUint[T util.UintType] []T // EncodeArray encodes a slice of uints using the Encoder interface. -func (arr uints) EncodeArray(enc Encoder) { +func (arr sliceOfUint[T]) EncodeArray(enc Encoder) { for _, v := range arr { enc.AppendUint64(uint64(v)) } } // Uints creates a Field with a slice of unsigned integers. -func Uints(key string, val []uint) Field { - return Array(key, uints(val)) -} - -type uint8s []uint8 - -// EncodeArray encodes a slice of uint8s using the Encoder interface. -func (arr uint8s) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendUint64(uint64(v)) - } -} - -// Uint8s creates a Field with a slice of uint8 values. -func Uint8s(key string, val []uint8) Field { - return Array(key, uint8s(val)) -} - -type uint16s []uint16 - -// EncodeArray encodes a slice of uint16s using the Encoder interface. -func (arr uint16s) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendUint64(uint64(v)) - } -} - -// Uint16s creates a Field with a slice of uint16 values. -func Uint16s(key string, val []uint16) Field { - return Array(key, uint16s(val)) +func Uints[T util.UintType](key string, val []T) Field { + return Array(key, sliceOfUint[T](val)) } -type uint32s []uint32 - -// EncodeArray encodes a slice of uint32s using the Encoder interface. -func (arr uint32s) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendUint64(uint64(v)) - } -} - -// Uint32s creates a Field with a slice of uint32 values. -func Uint32s(key string, val []uint32) Field { - return Array(key, uint32s(val)) -} - -type uint64s []uint64 - -// EncodeArray encodes a slice of uint64s using the Encoder interface. -func (arr uint64s) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendUint64(v) - } -} - -// Uint64s creates a Field with a slice of uint64 values. -func Uint64s(key string, val []uint64) Field { - return Array(key, uint64s(val)) -} - -type float32s []float32 +type sliceOfFloat[T util.FloatType] []T // EncodeArray encodes a slice of float32s using the Encoder interface. -func (arr float32s) EncodeArray(enc Encoder) { +func (arr sliceOfFloat[T]) EncodeArray(enc Encoder) { for _, v := range arr { enc.AppendFloat64(float64(v)) } } -// Float32s creates a Field with a slice of float32 values. -func Float32s(key string, val []float32) Field { - return Array(key, float32s(val)) -} - -type float64s []float64 - -// EncodeArray encodes a slice of float64s using the Encoder interface. -func (arr float64s) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendFloat64(v) - } -} - -// Float64s creates a Field with a slice of float64 values. -func Float64s(key string, val []float64) Field { - return Array(key, float64s(val)) +// Floats creates a Field with a slice of float32 values. +func Floats[T util.FloatType](key string, val []T) Field { + return Array(key, sliceOfFloat[T](val)) } type sliceOfString []string @@ -558,32 +281,32 @@ func Any(key string, value interface{}) Field { return Ints(key, val) case int8: - return Int8(key, val) + return Int(key, val) case *int8: - return Int8Ptr(key, val) + return IntPtr(key, val) case []int8: - return Int8s(key, val) + return Ints(key, val) case int16: - return Int16(key, val) + return Int(key, val) case *int16: - return Int16Ptr(key, val) + return IntPtr(key, val) case []int16: - return Int16s(key, val) + return Ints(key, val) case int32: - return Int32(key, val) + return Int(key, val) case *int32: - return Int32Ptr(key, val) + return IntPtr(key, val) case []int32: - return Int32s(key, val) + return Ints(key, val) case int64: - return Int64(key, val) + return Int(key, val) case *int64: - return Int64Ptr(key, val) + return IntPtr(key, val) case []int64: - return Int64s(key, val) + return Ints(key, val) case uint: return Uint(key, val) @@ -593,46 +316,46 @@ func Any(key string, value interface{}) Field { return Uints(key, val) case uint8: - return Uint8(key, val) + return Uint(key, val) case *uint8: - return Uint8Ptr(key, val) + return UintPtr(key, val) case []uint8: - return Uint8s(key, val) + return Uints(key, val) case uint16: - return Uint16(key, val) + return Uint(key, val) case *uint16: - return Uint16Ptr(key, val) + return UintPtr(key, val) case []uint16: - return Uint16s(key, val) + return Uints(key, val) case uint32: - return Uint32(key, val) + return Uint(key, val) case *uint32: - return Uint32Ptr(key, val) + return UintPtr(key, val) case []uint32: - return Uint32s(key, val) + return Uints(key, val) case uint64: - return Uint64(key, val) + return Uint(key, val) case *uint64: - return Uint64Ptr(key, val) + return UintPtr(key, val) case []uint64: - return Uint64s(key, val) + return Uints(key, val) case float32: - return Float32(key, val) + return Float(key, val) case *float32: - return Float32Ptr(key, val) + return FloatPtr(key, val) case []float32: - return Float32s(key, val) + return Floats(key, val) case float64: - return Float64(key, val) + return Float(key, val) case *float64: - return Float64Ptr(key, val) + return FloatPtr(key, val) case []float64: - return Float64s(key, val) + return Floats(key, val) case string: return String(key, val) diff --git a/log/field_encoder.go b/log/field_encoder.go index 1282cc2e..9fad3ff0 100644 --- a/log/field_encoder.go +++ b/log/field_encoder.go @@ -123,7 +123,7 @@ func (enc *JSONEncoder) AppendKey(key string) { enc.appendSeparator() enc.last = jsonTokenKey enc.buf.WriteByte('"') - SafeWriteString(enc.buf, key) + WriteLogString(enc.buf, key) enc.buf.WriteByte('"') enc.buf.WriteByte(':') } @@ -161,7 +161,7 @@ func (enc *JSONEncoder) AppendString(v string) { enc.appendSeparator() enc.last = jsonTokenValue enc.buf.WriteByte('"') - SafeWriteString(enc.buf, v) + WriteLogString(enc.buf, v) enc.buf.WriteByte('"') } @@ -172,7 +172,7 @@ func (enc *JSONEncoder) AppendReflect(v interface{}) { b, err := json.Marshal(v) if err != nil { enc.buf.WriteByte('"') - SafeWriteString(enc.buf, err.Error()) + WriteLogString(enc.buf, err.Error()) enc.buf.WriteByte('"') return } @@ -186,7 +186,7 @@ type TextEncoder struct { 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 + wroteField bool // Tracks if the first key-value has been written } // NewTextEncoder creates a new TextEncoder, using the specified separator. @@ -246,12 +246,12 @@ func (enc *TextEncoder) AppendKey(key string) { enc.jsonEncoder.AppendKey(key) return } - if enc.firstField { + if enc.wroteField { enc.buf.WriteString(enc.separator) } else { - enc.firstField = true + enc.wroteField = true } - SafeWriteString(enc.buf, key) + WriteLogString(enc.buf, key) enc.buf.WriteByte('=') } @@ -297,7 +297,7 @@ func (enc *TextEncoder) AppendString(v string) { enc.jsonEncoder.AppendString(v) return } - SafeWriteString(enc.buf, v) + WriteLogString(enc.buf, v) } // AppendReflect uses reflection to marshal any value as JSON. @@ -309,7 +309,7 @@ func (enc *TextEncoder) AppendReflect(v interface{}) { } b, err := json.Marshal(v) if err != nil { - SafeWriteString(enc.buf, err.Error()) + WriteLogString(enc.buf, err.Error()) return } enc.buf.Write(b) @@ -317,8 +317,8 @@ func (enc *TextEncoder) AppendReflect(v interface{}) { /************************************* string ********************************/ -// SafeWriteString escapes and writes a string according to JSON rules. -func SafeWriteString(buf *bytes.Buffer, s string) { +// WriteLogString escapes and writes a string according to JSON rules. +func WriteLogString(buf *bytes.Buffer, s string) { for i := 0; i < len(s); { // Try to add a single-byte (ASCII) character directly if tryAddRuneSelf(buf, s[i]) { diff --git a/log/field_test.go b/log/field_test.go index 9e8d77a4..053e5cb5 100644 --- a/log/field_test.go +++ b/log/field_test.go @@ -20,75 +20,72 @@ import ( "bytes" "testing" + "github.com/go-spring/spring-core/util" "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(true)), + Any("bool_ptr", util.Ptr(true)), 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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.Ptr("a")), Any("string_ptr_nil", (*string)(nil)), Any("string_slice", []string{"a", "b", "c"}), Object("object", Any("int64", int64(1)), Any("uint64", uint64(1)), Any("string", "a")), - Any("struct", struct{ Int64 int64 }{10}), + Reflect("struct", struct{ Int64 int64 }{10}), } func TestJSONEncoder(t *testing.T) { @@ -229,9 +226,9 @@ func TestJSONEncoder(t *testing.T) { "uint64": 1, "string": "a" }, - "struct": { - "Int64": 10 - } + "struct": { + "Int64": 10 + } }`) }) } diff --git a/log/log.go b/log/log.go index 50d97d89..eb6ad733 100644 --- a/log/log.go +++ b/log/log.go @@ -45,91 +45,93 @@ func Fields(fields map[string]any) []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()...) + Record(ctx, TraceLevel, tag, 2, fn()...) } } // Tracef logs a message at TraceLevel using a tag and a formatted message. func Tracef(ctx context.Context, tag *Tag, format string, args ...any) { if tag.GetLogger().EnableLevel(TraceLevel) { - Record(ctx, TraceLevel, tag, Msgf(format, args...)) + Record(ctx, TraceLevel, tag, 2, Msgf(format, args...)) } } // 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()...) + Record(ctx, DebugLevel, tag, 2, fn()...) } } // Debugf logs a message at DebugLevel using a tag and a formatted message. func Debugf(ctx context.Context, tag *Tag, format string, args ...any) { if tag.GetLogger().EnableLevel(DebugLevel) { - Record(ctx, DebugLevel, tag, Msgf(format, args...)) + Record(ctx, DebugLevel, tag, 2, Msgf(format, args...)) } } // Info logs a message at InfoLevel using structured fields. func Info(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, InfoLevel, tag, fields...) + Record(ctx, InfoLevel, tag, 2, fields...) } // Infof logs a message at InfoLevel using a formatted message. func Infof(ctx context.Context, tag *Tag, format string, args ...any) { - Record(ctx, InfoLevel, tag, Msgf(format, args...)) + Record(ctx, InfoLevel, tag, 2, Msgf(format, args...)) } // Warn logs a message at WarnLevel using structured fields. func Warn(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, WarnLevel, tag, fields...) + Record(ctx, WarnLevel, tag, 2, fields...) } // Warnf logs a message at WarnLevel using a formatted message. func Warnf(ctx context.Context, tag *Tag, format string, args ...any) { - Record(ctx, WarnLevel, tag, Msgf(format, args...)) + Record(ctx, WarnLevel, tag, 2, Msgf(format, args...)) } // Error logs a message at ErrorLevel using structured fields. func Error(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, ErrorLevel, tag, fields...) + Record(ctx, ErrorLevel, tag, 2, fields...) } // Errorf logs a message at ErrorLevel using a formatted message. func Errorf(ctx context.Context, tag *Tag, format string, args ...any) { - Record(ctx, ErrorLevel, tag, Msgf(format, args...)) + Record(ctx, ErrorLevel, tag, 2, Msgf(format, args...)) } // Panic logs a message at PanicLevel using structured fields. func Panic(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, PanicLevel, tag, fields...) + Record(ctx, PanicLevel, tag, 2, fields...) } // Panicf logs a message at PanicLevel using a formatted message. func Panicf(ctx context.Context, tag *Tag, format string, args ...any) { - Record(ctx, PanicLevel, tag, Msgf(format, args...)) + Record(ctx, PanicLevel, tag, 2, Msgf(format, args...)) } // Fatal logs a message at FatalLevel using structured fields. func Fatal(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, FatalLevel, tag, fields...) + Record(ctx, FatalLevel, tag, 2, fields...) } // Fatalf logs a message at FatalLevel using a formatted message. func Fatalf(ctx context.Context, tag *Tag, format string, args ...any) { - Record(ctx, FatalLevel, tag, Msgf(format, args...)) + Record(ctx, FatalLevel, tag, 2, Msgf(format, args...)) } // 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 +func Record(ctx context.Context, level Level, tag *Tag, skip int, fields ...Field) { + var l *Logger + + // Check if the logger is enabled for the given level + if l = tag.GetLogger(); !l.EnableLevel(level) { + return } - file, line := internal.Caller(2, true) + file, line := internal.Caller(skip, true) // Determine the log timestamp now := time.Now() @@ -159,5 +161,5 @@ func Record(ctx context.Context, level Level, tag *Tag, fields ...Field) { e.CtxString = ctxString e.CtxFields = ctxFields - logger.Publish(e) + l.Publish(e) } diff --git a/log/log_event.go b/log/log_event.go index 66b440d2..65fef551 100644 --- a/log/log_event.go +++ b/log/log_event.go @@ -39,6 +39,7 @@ type Event struct { CtxFields []Field // Additional fields derived from the context (e.g., request ID, user ID) } +// Reset clears the Event's fields so the instance can be reused. func (e *Event) Reset() { e.Level = NoneLevel e.Time = time.Time{} @@ -50,10 +51,13 @@ func (e *Event) Reset() { e.CtxFields = nil } +// GetEvent retrieves an *Event from the pool. +// If the pool is empty, a new Event will be created by the New function. func GetEvent() *Event { return eventPool.Get().(*Event) } +// PutEvent resets the given Event and returns it to the pool for reuse. func PutEvent(e *Event) { e.Reset() eventPool.Put(e) diff --git a/log/log_level.go b/log/log_level.go index c8b38fbd..a5f3f2bb 100644 --- a/log/log_level.go +++ b/log/log_level.go @@ -39,8 +39,8 @@ const ( // Level is an enumeration used to identify the severity of a logging event. type Level int32 -func (level Level) String() string { - switch level { +func (l Level) String() string { + switch l { case NoneLevel: return "NONE" case TraceLevel: @@ -64,8 +64,8 @@ func (level Level) String() string { // 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) { +func ParseLevel(s string) (Level, error) { + switch strings.ToUpper(s) { case "NONE": return NoneLevel, nil case "TRACE": @@ -83,6 +83,6 @@ func ParseLevel(str string) (Level, error) { case "FATAL": return FatalLevel, nil default: - return -1, fmt.Errorf("invalid level %s", str) + return -1, fmt.Errorf("invalid level %s", s) } } diff --git a/log/log_reader.go b/log/log_reader.go index ef6f79e8..f6af7d9c 100644 --- a/log/log_reader.go +++ b/log/log_reader.go @@ -129,7 +129,7 @@ func (r *XMLReader) Read(b []byte) (*Node, error) { parent := stack[len(stack)-2] parent.Children = append(parent.Children, curr) stack = stack[:len(stack)-1] - default: + default: // for linter } } if len(stack[0].Children) == 0 { diff --git a/log/plugin_logger.go b/log/plugin_logger.go index f39761be..5876db82 100644 --- a/log/plugin_logger.go +++ b/log/plugin_logger.go @@ -21,7 +21,7 @@ import ( ) // OnDropEvent is a callback function that is called when an event is dropped. -var OnDropEvent func(*Event) +var OnDropEvent func(logger string, e *Event) func init() { RegisterPlugin[AppenderRef]("AppenderRef", PluginTypeAppenderRef) @@ -127,7 +127,7 @@ func (c *AsyncLoggerConfig) Publish(e *Event) { // Drop the event if the buffer is full PutEvent(e) if OnDropEvent != nil { - OnDropEvent(e) + OnDropEvent(c.Name, e) } } } diff --git a/log/plugin_logger_test.go b/log/plugin_logger_test.go index 051482b5..9104a9f2 100644 --- a/log/plugin_logger_test.go +++ b/log/plugin_logger_test.go @@ -112,7 +112,7 @@ func TestAsyncLoggerConfig(t *testing.T) { assert.Nil(t, err) dropCount := 0 - OnDropEvent = func(*Event) { + OnDropEvent = func(logger string, e *Event) { dropCount++ } defer func() { 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 From 4d50a242e3ff6ddefbf19c75d59b352c52e1fea3 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Wed, 11 Jun 2025 06:57:45 +0800 Subject: [PATCH 13/18] feat(doc): add chatAI example --- doc/examples/chatAI/chatAI.html | 181 ++++++++++++++++++++++++++++++++ doc/examples/chatAI/go.mod | 15 +++ doc/examples/chatAI/go.sum | 26 +++++ doc/examples/chatAI/main.go | 73 +++++++++++++ 4 files changed, 295 insertions(+) create mode 100644 doc/examples/chatAI/chatAI.html create mode 100644 doc/examples/chatAI/go.mod create mode 100644 doc/examples/chatAI/go.sum create mode 100644 doc/examples/chatAI/main.go diff --git a/doc/examples/chatAI/chatAI.html b/doc/examples/chatAI/chatAI.html new file mode 100644 index 00000000..6afd2246 --- /dev/null +++ b/doc/examples/chatAI/chatAI.html @@ -0,0 +1,181 @@ + + + + + Chat AI + + + +
+

🧠 Chat AI

+
+ + +
+
+
+ + + + diff --git a/doc/examples/chatAI/go.mod b/doc/examples/chatAI/go.mod new file mode 100644 index 00000000..bc244dac --- /dev/null +++ b/doc/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/chatAI/go.sum b/doc/examples/chatAI/go.sum new file mode 100644 index 00000000..77e1730d --- /dev/null +++ b/doc/examples/chatAI/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/chatAI/main.go b/doc/examples/chatAI/main.go new file mode 100644 index 00000000..d6853c7e --- /dev/null +++ b/doc/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 From 09ae5065b062fcce76e718f595d07662fb7b3495 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Wed, 11 Jun 2025 10:20:34 +0800 Subject: [PATCH 14/18] feat(log): add _gs/_def/_biz tag --- gs/gs.go | 11 ++- gs/internal/gs_app/app.go | 16 ++--- gs/internal/gs_app/app_test.go | 22 +++--- gs/internal/gs_app/boot_test.go | 6 +- gs/internal/gs_conf/conf.go | 12 ++-- gs/internal/gs_conf/conf_test.go | 7 +- gs/internal/gs_core/injecting/injecting.go | 17 ++--- gs/prop.go | 16 ++--- log/log.go | 6 ++ log/log_tag.go | 11 ++- log/log_tag_test.go | 15 +++- log/plugin_appender.go | 6 +- log/plugin_appender_test.go | 5 +- log/plugin_test.go | 2 +- util/goutil/goutil.go | 5 +- util/sysconf/sysconf.go | 82 ---------------------- util/sysconf/sysconf_test.go | 41 ----------- util/syslog/syslog.go | 75 -------------------- util/syslog/syslog_test.go | 30 -------- 19 files changed, 99 insertions(+), 286 deletions(-) delete mode 100644 util/sysconf/sysconf.go delete mode 100644 util/sysconf/sysconf_test.go delete mode 100644 util/syslog/syslog.go delete mode 100644 util/syslog/syslog_test.go diff --git a/gs/gs.go b/gs/gs.go index 16fef869..19a28e02 100644 --- a/gs/gs.go +++ b/gs/gs.go @@ -30,7 +30,7 @@ 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" + "github.com/go-spring/spring-core/log" ) const ( @@ -172,6 +172,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.TagGS, "failed to set property key=%s, err=%v", key, err) + } +} + type ( Runner = gs.Runner Job = gs.Job @@ -223,7 +230,7 @@ func (s *AppStarter) RunWith(fn func(ctx context.Context) error) { var err error defer func() { if err != nil { - syslog.Errorf("app run failed: %s", err.Error()) + log.Errorf(context.Background(), log.TagGS, "app run failed: %v", err) } }() printBanner() diff --git a/gs/internal/gs_app/app.go b/gs/internal/gs_app/app.go index 8e671cac..347646c2 100644 --- a/gs/internal/gs_app/app.go +++ b/gs/internal/gs_app/app.go @@ -31,8 +31,8 @@ import ( "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/log" "github.com/go-spring/spring-core/util/goutil" - "github.com/go-spring/spring-core/util/syslog" ) // GS is the global application instance. @@ -97,7 +97,7 @@ func (app *App) RunWith(fn func(ctx context.Context) error) error { 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.TagGS, "Received signal: %v", sig) app.ShutDown() }() @@ -145,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.TagGS, "job run error: %v", err) app.ShutDown() } }) @@ -169,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.TagGS, "server serve error: %v", err) sig.Intercept() app.ShutDown() } @@ -179,7 +179,7 @@ func (app *App) Start() error { if sig.Intercepted() { return nil } - syslog.Infof("ready to serve requests") + log.Infof(context.Background(), log.TagGS, "ready to serve requests") sig.Close() } return nil @@ -196,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.TagGS, "shutdown server failed: %v", err) } }) } @@ -207,9 +207,9 @@ func (app *App) Stop() { select { case <-waitChan: - syslog.Infof("shutdown complete") + log.Infof(context.Background(), log.TagGS, "shutdown complete") case <-ctx.Done(): - syslog.Infof("shutdown timeout") + log.Infof(context.Background(), log.TagGS, "shutdown timeout") } } diff --git a/gs/internal/gs_app/app_test.go b/gs/internal/gs_app/app_test.go index cee35f0f..62d585f9 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/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/log" + "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.TagGS, "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..2247f8ee 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 @@ -114,7 +116,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("sys", SysConf), NewNamedPropertyCopier("env", c.Environment), NewNamedPropertyCopier("cmd", c.CommandArgs), ) @@ -133,7 +135,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)) @@ -164,7 +166,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("sys", SysConf), NewNamedPropertyCopier("env", c.Environment), NewNamedPropertyCopier("cmd", c.CommandArgs), ) @@ -178,7 +180,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..616550fd 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" @@ -33,8 +34,8 @@ import ( "github.com/go-spring/spring-core/gs/internal/gs_bean" "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/log" "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.TagGS, "%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.TagGS, "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.TagGS, "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.TagGS, "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.TagGS, "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.TagGS, "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.TagGS, "%v", out[0].Interface()) } } } 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/log.go b/log/log.go index eb6ad733..3bb99cf8 100644 --- a/log/log.go +++ b/log/log.go @@ -24,6 +24,12 @@ import ( "github.com/go-spring/spring-core/util" ) +var ( + TagGS = GetTag("_gs") + TagDef = GetTag("_def") + TagBiz = GetTag("_biz") +) + // 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 diff --git a/log/log_tag.go b/log/log_tag.go index 8223d4bf..ce230cdf 100644 --- a/log/log_tag.go +++ b/log/log_tag.go @@ -19,6 +19,8 @@ package log import ( "strings" "sync/atomic" + + "github.com/go-spring/spring-core/util" ) var tagMap = map[string]*Tag{} @@ -70,13 +72,13 @@ func (m *Tag) SetLogger(logger *Logger) { } // isValidTag checks whether the tag is valid according to the following rules: -// 1. The length must be between 4 and 36 characters. +// 1. The length must be between 3 and 36 characters. // 2. Only lowercase letters (a-z), digits (0-9), and underscores (_) are allowed. // 3. The tag can start with an underscore. // 4. Underscores separate the tag into 1 to 4 non-empty segments. // 5. No empty segments are allowed (i.e., no consecutive or trailing underscores). func isValidTag(tag string) bool { - if len(tag) < 4 || len(tag) > 36 { + if len(tag) < 3 || len(tag) > 36 { return false } for i := 0; i < len(tag); i++ { @@ -111,3 +113,8 @@ func GetTag(tag string) *Tag { } return m } + +// GetAllTags returns all registered tags. +func GetAllTags() []string { + return util.OrderedMapKeys(tagMap) +} diff --git a/log/log_tag_test.go b/log/log_tag_test.go index 5ec75e47..c861586f 100644 --- a/log/log_tag_test.go +++ b/log/log_tag_test.go @@ -19,6 +19,8 @@ package log import ( "strings" "testing" + + "github.com/lvan100/go-assert" ) func TestIsValidTag(t *testing.T) { @@ -29,7 +31,7 @@ func TestIsValidTag(t *testing.T) { }{ {"valid_1segments", "_def", true}, {"valid_4segments", "service_module_submodule_component00", true}, - {"too_short_3", "abc", false}, + {"too_short_2", "ab", false}, {"too_long_37", strings.Repeat("a", 37), false}, {"uppercase", "Invalid_Tag", false}, {"special_char", "tag!name", false}, @@ -50,3 +52,14 @@ func TestIsValidTag(t *testing.T) { }) } } + +func TestGetAllTags(t *testing.T) { + tags := GetAllTags() + assert.That(t, tags).Equal([]string{ + "_biz", + "_com_request_in", + "_com_request_out", + "_def", + "_gs", + }) +} diff --git a/log/plugin_appender.go b/log/plugin_appender.go index b1852dca..bba8b395 100644 --- a/log/plugin_appender.go +++ b/log/plugin_appender.go @@ -17,9 +17,13 @@ package log import ( + "io" "os" ) +// Stdout is the standard output stream used by appenders. +var Stdout io.Writer = os.Stdout + func init() { RegisterPlugin[DiscardAppender]("Discard", PluginTypeAppender) RegisterPlugin[ConsoleAppender]("Console", PluginTypeAppender) @@ -63,7 +67,7 @@ type ConsoleAppender struct { // 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) + _, _ = Stdout.Write(data) } // FileAppender writes formatted log events to a specified file. diff --git a/log/plugin_appender_test.go b/log/plugin_appender_test.go index 9bb53b00..83da3585 100644 --- a/log/plugin_appender_test.go +++ b/log/plugin_appender_test.go @@ -38,10 +38,9 @@ func TestConsoleAppender(t *testing.T) { file, err := os.CreateTemp(os.TempDir(), "") assert.Nil(t, err) - oldStdout := os.Stdout - os.Stdout = file + Stdout = file defer func() { - os.Stdout = oldStdout + Stdout = os.Stdout }() a := &ConsoleAppender{ diff --git a/log/plugin_test.go b/log/plugin_test.go index 25662386..8d1570d3 100644 --- a/log/plugin_test.go +++ b/log/plugin_test.go @@ -29,7 +29,7 @@ func TestRegisterPlugin(t *testing.T) { }, "T must be struct") assert.Panic(t, func() { RegisterPlugin[FileAppender]("File", PluginTypeAppender) - }, "duplicate plugin Appender in .*/plugin_appender.go:26 and .*/plugin_test.go:31") + }, "duplicate plugin Appender in .*/plugin_appender.go:30 and .*/plugin_test.go:31") } func TestInjectAttribute(t *testing.T) { 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") -} From eea9a4765d74090051514dc1401c0c0c80eba54d Mon Sep 17 00:00:00 2001 From: lvan100 Date: Wed, 11 Jun 2025 13:37:14 +0800 Subject: [PATCH 15/18] refactor(log): optimize log refresh logic and exception handling --- conf/expr_test.go | 2 +- log/field.go | 4 +-- log/field_encoder.go | 6 ++-- log/internal/caller_test.go | 6 ++-- log/log_reader.go | 2 +- log/log_refresh.go | 8 +++-- log/log_refresh_test.go | 7 ++++- log/log_tag.go | 10 ++---- log/log_test.go | 62 +++++++++++++++++++++++++++++++------ log/plugin.go | 4 +-- log/plugin_appender.go | 1 + log/plugin_layout.go | 14 ++++----- log/plugin_logger.go | 3 +- log/plugin_logger_test.go | 6 ++-- util/color/color.go | 10 +++--- 15 files changed, 98 insertions(+), 47 deletions(-) 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/log/field.go b/log/field.go index fa7e9c91..4c9b79f3 100644 --- a/log/field.go +++ b/log/field.go @@ -157,7 +157,7 @@ func StringPtr(key string, val *string) Field { } // Reflect wraps any value into a Field using reflection. -func Reflect(key string, val interface{}) Field { +func Reflect(key string, val any) Field { return Field{ Key: key, Type: ValueTypeReflect, @@ -261,7 +261,7 @@ func Object(key string, fields ...Field) Field { // 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 { +func Any(key string, value any) Field { switch val := value.(type) { case nil: return Nil(key) diff --git a/log/field_encoder.go b/log/field_encoder.go index 9fad3ff0..99d4e68c 100644 --- a/log/field_encoder.go +++ b/log/field_encoder.go @@ -37,7 +37,7 @@ type Encoder interface { AppendUint64(v uint64) AppendFloat64(v float64) AppendString(v string) - AppendReflect(v interface{}) + AppendReflect(v any) } var ( @@ -166,7 +166,7 @@ func (enc *JSONEncoder) AppendString(v string) { } // AppendReflect marshals any Go value into JSON and appends it. -func (enc *JSONEncoder) AppendReflect(v interface{}) { +func (enc *JSONEncoder) AppendReflect(v any) { enc.appendSeparator() enc.last = jsonTokenValue b, err := json.Marshal(v) @@ -302,7 +302,7 @@ func (enc *TextEncoder) AppendString(v string) { // AppendReflect uses reflection to marshal any value as JSON. // If nested, delegates to JSON encoder. -func (enc *TextEncoder) AppendReflect(v interface{}) { +func (enc *TextEncoder) AppendReflect(v any) { if enc.jsonDepth > 0 { enc.jsonEncoder.AppendReflect(v) return diff --git a/log/internal/caller_test.go b/log/internal/caller_test.go index 253de7a0..8419bcc9 100644 --- a/log/internal/caller_test.go +++ b/log/internal/caller_test.go @@ -38,7 +38,7 @@ func TestCaller(t *testing.T) { }) t.Run("fast true", func(t *testing.T) { - for i := 0; i < 2; i++ { + for range 2 { file, line := internal.Caller(0, true) assert.ThatString(t, file).Matches(".*/caller_test.go") assert.That(t, line).Equal(42) @@ -52,13 +52,13 @@ func BenchmarkCaller(b *testing.B) { // BenchmarkCaller/slow-8 6314623 190.3 ns/op b.Run("fast", func(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { internal.Caller(0, true) } }) b.Run("slow", func(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { internal.Caller(0, false) } }) diff --git a/log/log_reader.go b/log/log_reader.go index f6af7d9c..ede887c6 100644 --- a/log/log_reader.go +++ b/log/log_reader.go @@ -54,7 +54,7 @@ func (node *Node) getChildren(label string) []*Node { // 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++ { + for range indent { buf.WriteString("\t") } buf.WriteString(node.Label) diff --git a/log/log_refresh.go b/log/log_refresh.go index 922b8491..ee4d2025 100644 --- a/log/log_refresh.go +++ b/log/log_refresh.go @@ -162,6 +162,7 @@ func RefreshReader(input io.Reader, ext string) error { base = &config.baseLoggerConfig case *AsyncLoggerConfig: base = &config.baseLoggerConfig + default: // for linter } for _, r := range base.AppenderRefs { @@ -178,14 +179,14 @@ func RefreshReader(input io.Reader, ext string) error { } } else { var ss []string - for _, s := range strings.Split(base.Tags, ",") { + for s := range strings.SplitSeq(base.Tags, ",") { if s = strings.TrimSpace(s); s == "" { continue } ss = append(ss, s) } if len(ss) == 0 { - return fmt.Errorf("RefreshReader: logger must have attribute 'tags' except root logger") + return fmt.Errorf("RefreshReader: logger must have attribute 'tags'") } for _, s := range ss { cTags[s] = logger @@ -211,6 +212,9 @@ func RefreshReader(input io.Reader, ext string) error { logArray = append(logArray, cTags[s]) } + // todo(lvan100): currently, there is only one refresh operation, + // so exception handling is temporarily ignored. + for _, a := range cAppenders { if err := a.Start(); err != nil { return errutil.WrapError(err, "RefreshReader: appender %s start error", a.GetName()) diff --git a/log/log_refresh_test.go b/log/log_refresh_test.go index 7c03c738..99faa50d 100644 --- a/log/log_refresh_test.go +++ b/log/log_refresh_test.go @@ -31,6 +31,11 @@ func (r funcReader) Read(p []byte) (n int, err error) { } func TestRefresh(t *testing.T) { + t.Cleanup(func() { + for _, tag := range tagMap { + tag.SetLogger(initLogger) + } + }) t.Run("file not exist", func(t *testing.T) { defer func() { initOnce.Store(false) }() @@ -316,7 +321,7 @@ func TestRefresh(t *testing.T) {
`), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: logger must have attribute 'tags' except root logger") + assert.ThatError(t, err).Matches("RefreshReader: logger must have attribute 'tags'") }) t.Run("Root error - 1", func(t *testing.T) { diff --git a/log/log_tag.go b/log/log_tag.go index ce230cdf..87615bd9 100644 --- a/log/log_tag.go +++ b/log/log_tag.go @@ -17,6 +17,7 @@ package log import ( + "slices" "strings" "sync/atomic" @@ -81,7 +82,7 @@ func isValidTag(tag string) bool { if len(tag) < 3 || len(tag) > 36 { return false } - for i := 0; i < len(tag); i++ { + for i := range len(tag) { c := tag[i] if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '_' { return false @@ -91,12 +92,7 @@ func isValidTag(tag string) bool { if len(ss) < 1 || len(ss) > 4 { return false } - for _, s := range ss { - if s == "" { - return false - } - } - return true + return !slices.Contains(ss, "") } // GetTag creates or retrieves a Tag by name. diff --git a/log/log_test.go b/log/log_test.go index b49ab5b7..7b826d3a 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -17,7 +17,10 @@ package log_test import ( + "bytes" "context" + "os" + "strings" "testing" "time" @@ -25,8 +28,10 @@ import ( "github.com/lvan100/go-assert" ) -const TraceID = "trace_id" -const SpanID = "span_id" +var ( + keyTraceID int + keySpanID int +) var TagDefault = log.GetTag("_def") var TagRequestIn = log.GetTag("_com_request_in") @@ -35,8 +40,14 @@ var TagRequestOut = log.GetTag("_com_request_out") func TestLog(t *testing.T) { ctx := t.Context() + logBuf := bytes.NewBuffer(nil) + log.Stdout = logBuf + defer func() { + log.Stdout = os.Stdout + }() + log.TimeNow = func(ctx context.Context) time.Time { - return time.Now() + return time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC) } log.StringFromContext = func(ctx context.Context) string { @@ -44,60 +55,93 @@ func TestLog(t *testing.T) { } log.FieldsFromContext = func(ctx context.Context) []log.Field { - traceID, _ := ctx.Value(TraceID).(string) - spanID, _ := ctx.Value(SpanID).(string) + traceID, _ := ctx.Value(&keyTraceID).(string) + spanID, _ := ctx.Value(&keySpanID).(string) return []log.Field{ - log.String(TraceID, traceID), - log.String(SpanID, spanID), + log.String("trace_id", traceID), + log.String("span_id", spanID), } } + // not print log.Debug(ctx, TagRequestOut, func() []log.Field { return []log.Field{ log.Msgf("hello %s", "world"), } }) + // print log.Info(ctx, TagDefault, log.Msgf("hello %s", "world")) log.Info(ctx, TagRequestIn, log.Msgf("hello %s", "world")) err := log.RefreshFile("testdata/log.xml") assert.Nil(t, err) - ctx = context.WithValue(ctx, TraceID, "0a882193682db71edd48044db54cae88") - ctx = context.WithValue(ctx, SpanID, "50ef0724418c0a66") + ctx = context.WithValue(ctx, &keyTraceID, "0a882193682db71edd48044db54cae88") + ctx = context.WithValue(ctx, &keySpanID, "50ef0724418c0a66") + // print log.Trace(ctx, TagRequestOut, func() []log.Field { return []log.Field{ log.Msgf("hello %s", "world"), } }) + // print log.Debug(ctx, TagRequestOut, func() []log.Field { return []log.Field{ log.Msgf("hello %s", "world"), } }) + // print log.Info(ctx, TagRequestIn, log.Msgf("hello %s", "world")) log.Warn(ctx, TagRequestIn, log.Msgf("hello %s", "world")) log.Error(ctx, TagRequestIn, log.Msgf("hello %s", "world")) log.Panic(ctx, TagRequestIn, log.Msgf("hello %s", "world")) log.Fatal(ctx, TagRequestIn, log.Msgf("hello %s", "world")) + // print log.Infof(ctx, TagRequestIn, "hello %s", "world") log.Warnf(ctx, TagRequestIn, "hello %s", "world") log.Errorf(ctx, TagRequestIn, "hello %s", "world") log.Panicf(ctx, TagRequestIn, "hello %s", "world") log.Fatalf(ctx, TagRequestIn, "hello %s", "world") + // not print log.Info(ctx, TagDefault, log.Msgf("hello %s", "world")) + + // print log.Warn(ctx, TagDefault, log.Msgf("hello %s", "world")) log.Error(ctx, TagDefault, log.Msgf("hello %s", "world")) log.Panic(ctx, TagDefault, log.Msgf("hello %s", "world")) + // print log.Error(ctx, TagDefault, log.Fields(map[string]any{ "key1": "value1", "key2": "value2", })...) + + expectLog := ` +[INFO][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:74] _def||trace_id=||span_id=||msg=hello world +[INFO][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:75] _com_request_in||trace_id=||span_id=||msg=hello world +{"level":"trace","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:84","tag":"_com_request_out","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"debug","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:91","tag":"_com_request_out","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"info","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:98","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"warn","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:99","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"error","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:100","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"panic","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:101","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"fatal","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:102","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"info","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:105","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"warn","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:106","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"error","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:107","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"panic","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:108","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +{"level":"fatal","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:109","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} +[WARN][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:115] _def||trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66||msg=hello world +[ERROR][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:116] _def||trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66||msg=hello world +[PANIC][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:117] _def||trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66||msg=hello world +[ERROR][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:120] _def||trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66||key1=value1||key2=value2 +` + + assert.ThatString(t, logBuf.String()).Equal(strings.TrimLeft(expectLog, "\n")) } diff --git a/log/plugin.go b/log/plugin.go index f1483d21..eaba24f0 100644 --- a/log/plugin.go +++ b/log/plugin.go @@ -97,7 +97,7 @@ func NewPlugin(t reflect.Type, node *Node, properties map[string]string) (reflec // 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++ { + for i := range v.NumField() { ft := t.Field(i) fv := v.Field(i) if tag, ok := ft.Tag.Lookup("PluginAttribute"); ok { @@ -266,7 +266,7 @@ func injectElement(tag string, fv reflect.Value, ft reflect.StructField, node *N switch fv.Kind() { case reflect.Slice: slice := reflect.MakeSlice(ft.Type, 0, len(children)) - for j := 0; j < len(children); j++ { + for j := range len(children) { slice = reflect.Append(slice, children[j]) } fv.Set(slice) diff --git a/log/plugin_appender.go b/log/plugin_appender.go index bba8b395..8b3bf925 100644 --- a/log/plugin_appender.go +++ b/log/plugin_appender.go @@ -78,6 +78,7 @@ type FileAppender struct { file *os.File } +// Start opens the 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 { diff --git a/log/plugin_layout.go b/log/plugin_layout.go index 3136e7e9..c24b38bd 100644 --- a/log/plugin_layout.go +++ b/log/plugin_layout.go @@ -148,19 +148,19 @@ 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)) + headers := make([]Field, 0, 5) + headers = append(headers, String("level", strings.ToLower(e.Level.String()))) + headers = append(headers, String("time", e.Time.Format("2006-01-02T15:04:05.000"))) + headers = append(headers, String("fileLine", c.GetFileLine(e))) + headers = append(headers, String("tag", e.Tag)) if e.CtxString != "" { - fields = append(fields, String("ctxString", e.CtxString)) + headers = append(headers, String("ctxString", e.CtxString)) } enc := NewJSONEncoder(buf) enc.AppendEncoderBegin() - WriteFields(enc, fields) + WriteFields(enc, headers) WriteFields(enc, e.CtxFields) WriteFields(enc, e.Fields) enc.AppendEncoderEnd() diff --git a/log/plugin_logger.go b/log/plugin_logger.go index 5876db82..26edb1a9 100644 --- a/log/plugin_logger.go +++ b/log/plugin_logger.go @@ -125,10 +125,11 @@ func (c *AsyncLoggerConfig) Publish(e *Event) { case c.buf <- e: default: // Drop the event if the buffer is full - PutEvent(e) if OnDropEvent != nil { OnDropEvent(c.Name, e) } + // Return the event to the pool + PutEvent(e) } } diff --git a/log/plugin_logger_test.go b/log/plugin_logger_test.go index 9104a9f2..5b528caa 100644 --- a/log/plugin_logger_test.go +++ b/log/plugin_logger_test.go @@ -62,7 +62,7 @@ func TestLoggerConfig(t *testing.T) { assert.True(t, l.EnableLevel(PanicLevel)) assert.True(t, l.EnableLevel(FatalLevel)) - for i := 0; i < 5; i++ { + for range 5 { l.Publish(&Event{}) } @@ -133,7 +133,7 @@ func TestAsyncLoggerConfig(t *testing.T) { err = l.Start() assert.Nil(t, err) - for i := 0; i < 5000; i++ { + for range 5000 { l.Publish(GetEvent()) } @@ -167,7 +167,7 @@ func TestAsyncLoggerConfig(t *testing.T) { err = l.Start() assert.Nil(t, err) - for i := 0; i < 5; i++ { + for range 5 { l.Publish(GetEvent()) } 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(';') From 6bf5b2e284a6110d9224bfae48e86aa57bd40b19 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Thu, 12 Jun 2025 17:32:25 +0800 Subject: [PATCH 16/18] docs: initialize document structure --- .gitignore | 4 ++-- docs/0. overview/overview.md | 1 + docs/1. getting-started/getting-started.md | 1 + docs/2. concepts/concepts.md | 1 + docs/3. guides/guides.md | 1 + {doc/examples => docs/4. examples}/bookman/README.md | 0 {doc/examples => docs/4. examples}/bookman/README_CN.md | 0 .../4. examples}/bookman/conf/app-test.properties | 0 .../examples => docs/4. examples}/bookman/conf/app.properties | 0 {doc/examples => docs/4. examples}/bookman/go.mod | 0 {doc/examples => docs/4. examples}/bookman/go.sum | 0 {doc/examples => docs/4. examples}/bookman/init.go | 0 {doc/examples => docs/4. examples}/bookman/log/.keep | 0 {doc/examples => docs/4. examples}/bookman/main.go | 0 {doc/examples => docs/4. examples}/bookman/public/index.html | 0 {doc/examples => docs/4. examples}/bookman/src/app/app.go | 0 .../4. examples}/bookman/src/app/bootstrap/bootstrap.go | 0 .../4. examples}/bookman/src/app/common/handlers/log/log.go | 0 .../4. examples}/bookman/src/app/common/httpsvr/httpsvr.go | 0 .../bookman/src/app/controller/controller-book.go | 0 .../4. examples}/bookman/src/app/controller/controller.go | 0 {doc/examples => docs/4. examples}/bookman/src/biz/biz.go | 0 {doc/examples => docs/4. examples}/bookman/src/biz/job/job.go | 0 .../bookman/src/biz/service/book_service/book_service.go | 0 .../bookman/src/biz/service/book_service/book_service_test.go | 0 .../4. examples}/bookman/src/dao/book_dao/book_dao.go | 0 .../4. examples}/bookman/src/dao/book_dao/book_dao_test.go | 0 .../4. examples}/bookman/src/idl/http/proto/proto.go | 0 .../4. examples}/bookman/src/sdk/book_sdk/book_sdk.go | 0 {doc/examples => docs/4. examples}/chatAI/chatAI.html | 0 {doc/examples => docs/4. examples}/chatAI/go.mod | 0 {doc/examples => docs/4. examples}/chatAI/go.sum | 0 {doc/examples => docs/4. examples}/chatAI/main.go | 0 docs/4. examples/examples.md | 1 + {doc/examples => docs/4. examples}/miniapi/go.mod | 0 {doc/examples => docs/4. examples}/miniapi/go.sum | 0 {doc/examples => docs/4. examples}/miniapi/main.go | 0 {doc/examples => docs/4. examples}/noweb/go.mod | 0 {doc/examples => docs/4. examples}/noweb/go.sum | 0 {doc/examples => docs/4. examples}/noweb/main.go | 0 {doc/examples => docs/4. examples}/servers/gin/go.mod | 0 {doc/examples => docs/4. examples}/servers/gin/go.sum | 0 {doc/examples => docs/4. examples}/servers/gin/main.go | 0 {doc/examples => docs/4. examples}/servers/gin/server.go | 0 {doc/examples => docs/4. examples}/servers/grpc/go.mod | 0 {doc/examples => docs/4. examples}/servers/grpc/go.sum | 0 .../examples => docs/4. examples}/servers/grpc/idl/echo.proto | 0 .../4. examples}/servers/grpc/idl/proto/echo.pb.go | 0 .../4. examples}/servers/grpc/idl/proto/echo_grpc.pb.go | 0 {doc/examples => docs/4. examples}/servers/grpc/main.go | 0 {doc/examples => docs/4. examples}/servers/grpc/server.go | 0 {doc/examples => docs/4. examples}/servers/thrift/go.mod | 0 {doc/examples => docs/4. examples}/servers/thrift/go.sum | 0 .../4. examples}/servers/thrift/idl/echo.thrift | 0 .../servers/thrift/idl/proto/GoUnusedProtection__.go | 0 .../4. examples}/servers/thrift/idl/proto/echo-consts.go | 0 .../4. examples}/servers/thrift/idl/proto/echo.go | 0 {doc/examples => docs/4. examples}/servers/thrift/main.go | 0 {doc/examples => docs/4. examples}/servers/thrift/server.go | 0 {doc/examples => docs/4. examples}/startup/README.md | 0 {doc/examples => docs/4. examples}/startup/README_CN.md | 0 {doc/examples => docs/4. examples}/startup/go.mod | 0 {doc/examples => docs/4. examples}/startup/go.sum | 0 {doc/examples => docs/4. examples}/startup/main.go | 0 docs/5. advanced/advanced.md | 1 + docs/6. integrations/integrations.md | 1 + docs/7. faq.md | 1 + docs/8. contributing.md | 1 + docs/9. changelog.md | 1 + 69 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 docs/0. overview/overview.md create mode 100644 docs/1. getting-started/getting-started.md create mode 100644 docs/2. concepts/concepts.md create mode 100644 docs/3. guides/guides.md rename {doc/examples => docs/4. examples}/bookman/README.md (100%) rename {doc/examples => docs/4. examples}/bookman/README_CN.md (100%) rename {doc/examples => docs/4. examples}/bookman/conf/app-test.properties (100%) rename {doc/examples => docs/4. examples}/bookman/conf/app.properties (100%) rename {doc/examples => docs/4. examples}/bookman/go.mod (100%) rename {doc/examples => docs/4. examples}/bookman/go.sum (100%) rename {doc/examples => docs/4. examples}/bookman/init.go (100%) rename {doc/examples => docs/4. examples}/bookman/log/.keep (100%) rename {doc/examples => docs/4. examples}/bookman/main.go (100%) rename {doc/examples => docs/4. examples}/bookman/public/index.html (100%) rename {doc/examples => docs/4. examples}/bookman/src/app/app.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/app/bootstrap/bootstrap.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/app/common/handlers/log/log.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/app/common/httpsvr/httpsvr.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/app/controller/controller-book.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/app/controller/controller.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/biz/biz.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/biz/job/job.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/biz/service/book_service/book_service.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/biz/service/book_service/book_service_test.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/dao/book_dao/book_dao.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/dao/book_dao/book_dao_test.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/idl/http/proto/proto.go (100%) rename {doc/examples => docs/4. examples}/bookman/src/sdk/book_sdk/book_sdk.go (100%) rename {doc/examples => docs/4. examples}/chatAI/chatAI.html (100%) rename {doc/examples => docs/4. examples}/chatAI/go.mod (100%) rename {doc/examples => docs/4. examples}/chatAI/go.sum (100%) rename {doc/examples => docs/4. examples}/chatAI/main.go (100%) create mode 100644 docs/4. examples/examples.md rename {doc/examples => docs/4. examples}/miniapi/go.mod (100%) rename {doc/examples => docs/4. examples}/miniapi/go.sum (100%) rename {doc/examples => docs/4. examples}/miniapi/main.go (100%) rename {doc/examples => docs/4. examples}/noweb/go.mod (100%) rename {doc/examples => docs/4. examples}/noweb/go.sum (100%) rename {doc/examples => docs/4. examples}/noweb/main.go (100%) rename {doc/examples => docs/4. examples}/servers/gin/go.mod (100%) rename {doc/examples => docs/4. examples}/servers/gin/go.sum (100%) rename {doc/examples => docs/4. examples}/servers/gin/main.go (100%) rename {doc/examples => docs/4. examples}/servers/gin/server.go (100%) rename {doc/examples => docs/4. examples}/servers/grpc/go.mod (100%) rename {doc/examples => docs/4. examples}/servers/grpc/go.sum (100%) rename {doc/examples => docs/4. examples}/servers/grpc/idl/echo.proto (100%) rename {doc/examples => docs/4. examples}/servers/grpc/idl/proto/echo.pb.go (100%) rename {doc/examples => docs/4. examples}/servers/grpc/idl/proto/echo_grpc.pb.go (100%) rename {doc/examples => docs/4. examples}/servers/grpc/main.go (100%) rename {doc/examples => docs/4. examples}/servers/grpc/server.go (100%) rename {doc/examples => docs/4. examples}/servers/thrift/go.mod (100%) rename {doc/examples => docs/4. examples}/servers/thrift/go.sum (100%) rename {doc/examples => docs/4. examples}/servers/thrift/idl/echo.thrift (100%) rename {doc/examples => docs/4. examples}/servers/thrift/idl/proto/GoUnusedProtection__.go (100%) rename {doc/examples => docs/4. examples}/servers/thrift/idl/proto/echo-consts.go (100%) rename {doc/examples => docs/4. examples}/servers/thrift/idl/proto/echo.go (100%) rename {doc/examples => docs/4. examples}/servers/thrift/main.go (100%) rename {doc/examples => docs/4. examples}/servers/thrift/server.go (100%) rename {doc/examples => docs/4. examples}/startup/README.md (100%) rename {doc/examples => docs/4. examples}/startup/README_CN.md (100%) rename {doc/examples => docs/4. examples}/startup/go.mod (100%) rename {doc/examples => docs/4. examples}/startup/go.sum (100%) rename {doc/examples => docs/4. examples}/startup/main.go (100%) create mode 100644 docs/5. advanced/advanced.md create mode 100644 docs/6. integrations/integrations.md create mode 100644 docs/7. faq.md create mode 100644 docs/8. contributing.md create mode 100644 docs/9. changelog.md diff --git a/.gitignore b/.gitignore index 5f19d07f..a9bb854a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,8 +32,8 @@ go.work.sum /conf/remote/ -doc/examples/bookman/conf/ -doc/examples/bookman/log/*.log +docs/**/bookman/conf/ +docs/**/bookman/log/*.log coverage.txt 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/doc/examples/chatAI/chatAI.html b/docs/4. examples/chatAI/chatAI.html similarity index 100% rename from doc/examples/chatAI/chatAI.html rename to docs/4. examples/chatAI/chatAI.html diff --git a/doc/examples/chatAI/go.mod b/docs/4. examples/chatAI/go.mod similarity index 100% rename from doc/examples/chatAI/go.mod rename to docs/4. examples/chatAI/go.mod diff --git a/doc/examples/chatAI/go.sum b/docs/4. examples/chatAI/go.sum similarity index 100% rename from doc/examples/chatAI/go.sum rename to docs/4. examples/chatAI/go.sum diff --git a/doc/examples/chatAI/main.go b/docs/4. examples/chatAI/main.go similarity index 100% rename from doc/examples/chatAI/main.go rename to docs/4. examples/chatAI/main.go 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/miniapi/go.sum b/docs/4. examples/miniapi/go.sum similarity index 100% rename from doc/examples/miniapi/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 100% rename from doc/examples/miniapi/main.go rename to docs/4. examples/miniapi/main.go 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/noweb/go.sum b/docs/4. examples/noweb/go.sum similarity index 100% rename from doc/examples/noweb/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 100% rename from doc/examples/noweb/main.go rename to docs/4. examples/noweb/main.go diff --git a/doc/examples/servers/gin/go.mod b/docs/4. examples/servers/gin/go.mod similarity index 100% rename from doc/examples/servers/gin/go.mod rename to docs/4. examples/servers/gin/go.mod diff --git a/doc/examples/servers/gin/go.sum b/docs/4. examples/servers/gin/go.sum similarity index 100% rename from doc/examples/servers/gin/go.sum rename to docs/4. examples/servers/gin/go.sum diff --git a/doc/examples/servers/gin/main.go b/docs/4. examples/servers/gin/main.go similarity index 100% rename from doc/examples/servers/gin/main.go rename to docs/4. examples/servers/gin/main.go diff --git a/doc/examples/servers/gin/server.go b/docs/4. examples/servers/gin/server.go similarity index 100% rename from doc/examples/servers/gin/server.go rename to docs/4. examples/servers/gin/server.go 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/doc/examples/startup/go.sum b/docs/4. examples/startup/go.sum similarity index 100% rename from doc/examples/startup/go.sum rename to docs/4. examples/startup/go.sum 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 From 9bbc4c395c7a7dd24b35d80cb786f176f1c77cb7 Mon Sep 17 00:00:00 2001 From: lvan100 Date: Thu, 12 Jun 2025 19:22:14 +0800 Subject: [PATCH 17/18] refactor(log): transferred log to go-spring/log --- go.mod | 1 + go.sum | 2 + gs/app.go | 69 +++ gs/banner.go | 72 +++ gs/gs.go | 89 +--- gs/internal/gs_app/app.go | 16 +- gs/internal/gs_app/app_test.go | 4 +- gs/internal/gs_conf/conf.go | 27 +- gs/internal/gs_core/injecting/injecting.go | 16 +- gs/log.go | 46 ++ log/benchmarks/fields/encoder/encoder.go | 161 ------ log/benchmarks/fields/field-value/field.go | 244 --------- .../fields/field-value/field_test.go | 212 -------- log/benchmarks/fields/go.mod | 3 - log/benchmarks/fields/main.go | 19 - log/benchmarks/fields/main_test.go | 170 ------ .../fields/value-interface/field.go | 120 ----- .../fields/value-interface/field_test.go | 77 --- .../fields/value-interface/value.go | 140 ----- log/benchmarks/fields/value-struct/field.go | 120 ----- .../fields/value-struct/field_test.go | 77 --- log/benchmarks/fields/value-struct/value.go | 207 -------- log/field.go | 405 -------------- log/field_encoder.go | 381 ------------- log/field_test.go | 287 ---------- log/internal/caller.go | 50 -- log/internal/caller_test.go | 65 --- log/log.go | 171 ------ log/log_event.go | 64 --- log/log_level.go | 88 --- log/log_level_test.go | 81 --- log/log_reader.go | 139 ----- log/log_reader_test.go | 88 --- log/log_refresh.go | 241 --------- log/log_refresh_test.go | 500 ------------------ log/log_tag.go | 116 ---- log/log_tag_test.go | 65 --- log/log_test.go | 147 ----- log/plugin.go | 283 ---------- log/plugin_appender.go | 102 ---- log/plugin_appender_test.go | 126 ----- log/plugin_layout.go | 170 ------ log/plugin_layout_test.go | 137 ----- log/plugin_logger.go | 140 ----- log/plugin_logger_test.go | 180 ------- log/plugin_test.go | 273 ---------- log/testdata/log.xml | 24 - 47 files changed, 227 insertions(+), 5988 deletions(-) create mode 100644 gs/app.go create mode 100644 gs/banner.go create mode 100644 gs/log.go delete mode 100644 log/benchmarks/fields/encoder/encoder.go delete mode 100644 log/benchmarks/fields/field-value/field.go delete mode 100644 log/benchmarks/fields/field-value/field_test.go delete mode 100644 log/benchmarks/fields/go.mod delete mode 100644 log/benchmarks/fields/main.go delete mode 100644 log/benchmarks/fields/main_test.go delete mode 100644 log/benchmarks/fields/value-interface/field.go delete mode 100644 log/benchmarks/fields/value-interface/field_test.go delete mode 100644 log/benchmarks/fields/value-interface/value.go delete mode 100644 log/benchmarks/fields/value-struct/field.go delete mode 100644 log/benchmarks/fields/value-struct/field_test.go delete mode 100644 log/benchmarks/fields/value-struct/value.go delete mode 100644 log/field.go delete mode 100644 log/field_encoder.go delete mode 100644 log/field_test.go delete mode 100644 log/internal/caller.go delete mode 100644 log/internal/caller_test.go delete mode 100644 log/log.go delete mode 100644 log/log_event.go delete mode 100644 log/log_level.go delete mode 100644 log/log_level_test.go delete mode 100644 log/log_reader.go delete mode 100644 log/log_reader_test.go delete mode 100644 log/log_refresh.go delete mode 100644 log/log_refresh_test.go delete mode 100644 log/log_tag.go delete mode 100644 log/log_tag_test.go delete mode 100644 log/log_test.go delete mode 100644 log/plugin.go delete mode 100644 log/plugin_appender.go delete mode 100644 log/plugin_appender_test.go delete mode 100644 log/plugin_layout.go delete mode 100644 log/plugin_layout_test.go delete mode 100644 log/plugin_logger.go delete mode 100644 log/plugin_logger_test.go delete mode 100644 log/plugin_test.go delete mode 100644 log/testdata/log.xml diff --git a/go.mod b/go.mod index 9295c256..e25e3f41 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ 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/magiconair/properties v1.8.10 github.com/pelletier/go-toml v1.9.5 diff --git a/go.sum b/go.sum index 77e1730d..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= 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 19a28e02..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/log" ) const ( @@ -175,7 +173,7 @@ func BeanSelectorFor[T any](name ...string) BeanSelector { // 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.TagGS, "failed to set property key=%s, err=%v", key, err) + log.Errorf(context.Background(), log.TagApp, "failed to set property key=%s, err=%v", key, err) } } @@ -212,48 +210,12 @@ 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() { - s.RunWith(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.TagGS, "app run failed: %v", err) - } - }() - printBanner() - if err = B.(*gs_app.BootImpl).Run(); err != nil { - return - } - B = nil - 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) { - printBanner() - if err := B.(*gs_app.BootImpl).Run(); err != nil { - return nil, err - } - B = nil - if err := gs_app.GS.Start(); err != nil { - return nil, err - } - return func() { gs_app.GS.Stop() }, nil -} - // Run runs the app and waits for an interrupt signal to exit. func Run() { new(AppStarter).Run() @@ -321,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/internal/gs_app/app.go b/gs/internal/gs_app/app.go index 347646c2..ea793b02 100644 --- a/gs/internal/gs_app/app.go +++ b/gs/internal/gs_app/app.go @@ -27,11 +27,11 @@ 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/log" "github.com/go-spring/spring-core/util/goutil" ) @@ -97,7 +97,7 @@ func (app *App) RunWith(fn func(ctx context.Context) error) error { ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) sig := <-ch - log.Infof(context.Background(), log.TagGS, "Received signal: %v", sig) + log.Infof(context.Background(), log.TagApp, "Received signal: %v", sig) app.ShutDown() }() @@ -145,7 +145,7 @@ func (app *App) Start() error { } }() if err := job.Run(app.ctx); err != nil { - log.Errorf(context.Background(), log.TagGS, "job run error: %v", err) + log.Errorf(context.Background(), log.TagApp, "job run error: %v", err) app.ShutDown() } }) @@ -169,7 +169,7 @@ func (app *App) Start() error { }() err := svr.ListenAndServe(sig) if err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Errorf(context.Background(), log.TagGS, "server serve error: %v", err) + log.Errorf(context.Background(), log.TagApp, "server serve error: %v", err) sig.Intercept() app.ShutDown() } @@ -179,7 +179,7 @@ func (app *App) Start() error { if sig.Intercepted() { return nil } - log.Infof(context.Background(), log.TagGS, "ready to serve requests") + log.Infof(context.Background(), log.TagApp, "ready to serve requests") sig.Close() } return nil @@ -196,7 +196,7 @@ func (app *App) Stop() { for _, svr := range app.Servers { goutil.GoFunc(func() { if err := svr.Shutdown(ctx); err != nil { - log.Errorf(context.Background(), log.TagGS, "shutdown server failed: %v", err) + log.Errorf(context.Background(), log.TagApp, "shutdown server failed: %v", err) } }) } @@ -207,9 +207,9 @@ func (app *App) Stop() { select { case <-waitChan: - log.Infof(context.Background(), log.TagGS, "shutdown complete") + log.Infof(context.Background(), log.TagApp, "shutdown complete") case <-ctx.Done(): - log.Infof(context.Background(), log.TagGS, "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 62d585f9..f6a7bba3 100644 --- a/gs/internal/gs_app/app_test.go +++ b/gs/internal/gs_app/app_test.go @@ -26,10 +26,10 @@ import ( "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/gs/internal/gs_conf" - "github.com/go-spring/spring-core/log" "github.com/go-spring/spring-core/util/goutil" "github.com/lvan100/go-assert" "go.uber.org/mock/gomock" @@ -39,7 +39,7 @@ var logBuf = &bytes.Buffer{} func init() { goutil.OnPanic = func(ctx context.Context, r any) { - log.Panicf(ctx, log.TagGS, "panic: %v\n%s\n", r, debug.Stack()) + log.Panicf(ctx, log.TagDef, "panic: %v\n%s\n", r, debug.Stack()) } } diff --git a/gs/internal/gs_conf/conf.go b/gs/internal/gs_conf/conf.go index 2247f8ee..91865d99 100644 --- a/gs/internal/gs_conf/conf.go +++ b/gs/internal/gs_conf/conf.go @@ -80,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. @@ -115,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), - NewNamedPropertyCopier("env", c.Environment), - NewNamedPropertyCopier("cmd", c.CommandArgs), - ) + p, err := new(SysConfig).Refresh() if err != nil { return nil, err } @@ -165,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), - NewNamedPropertyCopier("env", c.Environment), - NewNamedPropertyCopier("cmd", c.CommandArgs), - ) + p, err := new(SysConfig).Refresh() if err != nil { return nil, err } diff --git a/gs/internal/gs_core/injecting/injecting.go b/gs/internal/gs_core/injecting/injecting.go index 616550fd..9e9998be 100644 --- a/gs/internal/gs_core/injecting/injecting.go +++ b/gs/internal/gs_core/injecting/injecting.go @@ -28,13 +28,13 @@ 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" "github.com/go-spring/spring-core/gs/internal/gs_bean" "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/log" "github.com/go-spring/spring-core/util" "github.com/spf13/cast" ) @@ -99,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()) - log.Errorf(context.Background(), log.TagGS, "%v", err) + log.Errorf(context.Background(), log.TagApp, "%v", err) } }() @@ -272,7 +272,7 @@ func (c *Injector) getBean(t reflect.Type, tag WireTag, stack *Stack) (BeanRunti } if !slices.Contains(foundBeans, b) { foundBeans = append(foundBeans, b) - log.Warnf(context.Background(), log.TagGS, "you should call Export() on %s", b) + log.Warnf(context.Background(), log.TagApp, "you should call Export() on %s", b) } } } @@ -566,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 { - log.Warnf(context.Background(), log.TagGS, "autowire error: %v", err) + log.Warnf(context.Background(), log.TagApp, "autowire error: %v", err) return reflect.Value{}, nil } return reflect.Value{}, err @@ -576,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 { - log.Warnf(context.Background(), log.TagGS, "autowire error: %v", err) + log.Warnf(context.Background(), log.TagApp, "autowire error: %v", err) return reflect.Value{}, nil } return reflect.Value{}, i.(error) @@ -746,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) { - log.Debugf(context.Background(), log.TagGS, "push %s %s", b, b.Status()) + log.Debugf(context.Background(), log.TagApp, "push %s %s", b, b.Status()) s.beans = append(s.beans, b) } @@ -756,7 +756,7 @@ func (s *Stack) popBean() { b := s.beans[n-1] s.beans[n-1] = nil s.beans = s.beans[:n-1] - log.Debugf(context.Background(), log.TagGS, "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. @@ -810,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() { - log.Errorf(context.Background(), log.TagGS, "%v", out[0].Interface()) + 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/log/benchmarks/fields/encoder/encoder.go b/log/benchmarks/fields/encoder/encoder.go deleted file mode 100644 index d4ee2890..00000000 --- a/log/benchmarks/fields/encoder/encoder.go +++ /dev/null @@ -1,161 +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 encoder - -import ( - "bytes" - "encoding/json" - "strconv" -) - -// 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) - AppendFloat64(v float64) - AppendString(v string) - AppendReflect(v interface{}) -} - -// 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, - } -} - -// 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.buf.WriteString(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)) -} - -// 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.buf.WriteString(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.buf.WriteString(err.Error()) - enc.buf.WriteByte('"') - return - } - enc.buf.Write(b) -} diff --git a/log/benchmarks/fields/field-value/field.go b/log/benchmarks/fields/field-value/field.go deleted file mode 100644 index 481fff0a..00000000 --- a/log/benchmarks/fields/field-value/field.go +++ /dev/null @@ -1,244 +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 field_value - -import ( - "math" - - "benchmark-fields/encoder" -) - -type ValueType int - -const ( - ValueTypeBool = ValueType(iota) - ValueTypeInt64 - ValueTypeFloat64 - ValueTypeString - ValueTypeReflect - ValueTypeBools - ValueTypeInt64s - ValueTypeFloat64s - ValueTypeStrings - ValueTypeArray - ValueTypeObject -) - -// Field represents a structured log field with a key and it's value. -type Field struct { - Key string - Type ValueType - Num uint64 - Str string - Any any -} - -// 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 { - if val { - return Field{ - Key: key, - Type: ValueTypeBool, - Num: 1, - } - } - return Field{ - Key: key, - Type: ValueTypeBool, - Num: 0, - } -} - -// Int64 creates a Field for an int64 value. -func Int64(key string, val int64) Field { - return Field{ - Key: key, - Type: ValueTypeInt64, - Num: uint64(val), - } -} - -// Float64 creates a Field for a float64 value. -func Float64(key string, val float64) Field { - return Field{ - Key: key, - Type: ValueTypeFloat64, - Num: math.Float64bits(val), - } -} - -// String creates a Field for a string value. -func String(key string, val string) Field { - return Field{ - Key: key, - Type: ValueTypeString, - Str: val, - } -} - -// Reflect wraps any value into a Field using reflection. -func Reflect(key string, val interface{}) Field { - return Field{ - Key: key, - Type: ValueTypeReflect, - Any: val, - } -} - -// Bools creates a Field with a slice of booleans. -func Bools(key string, val []bool) Field { - return Field{ - Key: key, - Type: ValueTypeBools, - Any: val, - } -} - -// Int64s creates a Field with a slice of int64 values. -func Int64s(key string, val []int64) Field { - return Field{ - Key: key, - Type: ValueTypeInt64s, - Any: val, - } -} - -// Float64s creates a Field with a slice of float64 values. -func Float64s(key string, val []float64) Field { - return Field{ - Key: key, - Type: ValueTypeFloat64s, - Any: val, - } -} - -// Strings creates a Field with a slice of strings. -func Strings(key string, val []string) Field { - return Field{ - Key: key, - Type: ValueTypeStrings, - Any: val, - } -} - -// ArrayValue represents a slice of Values. -type ArrayValue interface { - EncodeArray(enc encoder.Encoder) -} - -// Array creates a Field containing a variadic slice of Values, wrapped as an array. -func Array(key string, val ArrayValue) Field { - return Field{ - Key: key, - Type: ValueTypeArray, - Any: 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, - Type: ValueTypeObject, - Any: fields, - } -} - -// 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 Bools(key, val) - case int64: - return Int64(key, val) - case []int64: - return Int64s(key, val) - case float64: - return Float64(key, val) - case []float64: - return Float64s(key, val) - case string: - return String(key, val) - case []string: - return Strings(key, val) - default: - return Reflect(key, val) - } -} - -// Encode encodes the data represented by v to an Encoder. -func (f *Field) Encode(enc encoder.Encoder) { - enc.AppendKey(f.Key) - switch f.Type { - case ValueTypeBool: - enc.AppendBool(f.Num != 0) - case ValueTypeInt64: - enc.AppendInt64(int64(f.Num)) - case ValueTypeFloat64: - enc.AppendFloat64(math.Float64frombits(f.Num)) - case ValueTypeString: - enc.AppendString(f.Str) - case ValueTypeReflect: - enc.AppendReflect(f.Any) - case ValueTypeBools: - enc.AppendArrayBegin() - for _, val := range f.Any.([]bool) { - enc.AppendBool(val) - } - enc.AppendArrayEnd() - case ValueTypeInt64s: - enc.AppendArrayBegin() - for _, val := range f.Any.([]int64) { - enc.AppendInt64(val) - } - enc.AppendArrayEnd() - case ValueTypeFloat64s: - enc.AppendArrayBegin() - for _, val := range f.Any.([]float64) { - enc.AppendFloat64(val) - } - enc.AppendArrayEnd() - case ValueTypeStrings: - enc.AppendArrayBegin() - for _, val := range f.Any.([]string) { - enc.AppendString(val) - } - enc.AppendArrayEnd() - case ValueTypeArray: - enc.AppendArrayBegin() - f.Any.(ArrayValue).EncodeArray(enc) - enc.AppendArrayEnd() - case ValueTypeObject: - enc.AppendObjectBegin() - for _, val := range f.Any.([]Field) { - val.Encode(enc) - } - enc.AppendObjectEnd() - default: // for linter - } -} diff --git a/log/benchmarks/fields/field-value/field_test.go b/log/benchmarks/fields/field-value/field_test.go deleted file mode 100644 index 3bcb32b8..00000000 --- a/log/benchmarks/fields/field-value/field_test.go +++ /dev/null @@ -1,212 +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 field_value - -import ( - "bytes" - "fmt" - "testing" - - "benchmark-fields/encoder" -) - -type bools []bool - -func (arr bools) EncodeArray(enc encoder.Encoder) { - for _, v := range arr { - enc.AppendBool(v) - } -} - -type int64s []int64 - -func (arr int64s) EncodeArray(enc encoder.Encoder) { - for _, v := range arr { - enc.AppendInt64(v) - } -} - -type float64s []float64 - -func (arr float64s) EncodeArray(enc encoder.Encoder) { - for _, v := range arr { - enc.AppendFloat64(v) - } -} - -type strings []string - -func (arr strings) EncodeArray(enc encoder.Encoder) { - for _, v := range arr { - enc.AppendString(v) - } -} - -func BenchmarkFieldValue(b *testing.B) { - - // bools-8 10706354 123.8 ns/op 152 B/op 4 allocs/op - // bools_as_ArrayValue-8 11192071 107.9 ns/op 152 B/op 4 allocs/op - // int64s-8 9132631 131.0 ns/op 152 B/op 4 allocs/op - // int64s_as_ArrayValue-8 9038806 131.8 ns/op 152 B/op 4 allocs/op - // float64s-8 2022433 597.8 ns/op 344 B/op 12 allocs/op - // float64s_as_ArrayValue-8 1988160 602.1 ns/op 344 B/op 12 allocs/op - // strings-8 8134866 145.5 ns/op 152 B/op 4 allocs/op - // strings_as_ArrayValue-8 8170756 165.6 ns/op 152 B/op 4 allocs/op - - arrBools := []bool{true, false, true, false, true, false} - arrInt64s := []int64{1, 2, 3, 4, 5, 6, 7, 8} - arrFloat64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8} - arrStrings := []string{"a", "b", "c", "d", "e", "f", "g", "h"} - - b1 := bytes.NewBuffer(nil) - b2 := bytes.NewBuffer(nil) - b3 := bytes.NewBuffer(nil) - b4 := bytes.NewBuffer(nil) - b5 := bytes.NewBuffer(nil) - b6 := bytes.NewBuffer(nil) - b7 := bytes.NewBuffer(nil) - b8 := bytes.NewBuffer(nil) - - { - v := Bools("arr", arrBools) - v.Encode(encoder.NewJSONEncoder(b1)) - } - - { - v := Array("arr", bools(arrBools)) - v.Encode(encoder.NewJSONEncoder(b2)) - } - - { - v := Int64s("arr", arrInt64s) - v.Encode(encoder.NewJSONEncoder(b3)) - } - - { - v := Array("arr", int64s(arrInt64s)) - v.Encode(encoder.NewJSONEncoder(b4)) - } - - { - v := Float64s("arr", arrFloat64s) - v.Encode(encoder.NewJSONEncoder(b5)) - } - - { - v := Array("arr", float64s(arrFloat64s)) - v.Encode(encoder.NewJSONEncoder(b6)) - } - - { - v := Strings("arr", arrStrings) - v.Encode(encoder.NewJSONEncoder(b7)) - } - - { - v := Array("arr", strings(arrStrings)) - v.Encode(encoder.NewJSONEncoder(b8)) - } - - fmt.Println(b1.String()) - fmt.Println(b2.String()) - fmt.Println(b3.String()) - fmt.Println(b4.String()) - fmt.Println(b5.String()) - fmt.Println(b6.String()) - fmt.Println(b7.String()) - fmt.Println(b8.String()) - - b.Run("bools", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Bools("arr", arrBools) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("bools as ArrayValue", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Array("arr", bools(arrBools)) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("int64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Int64s("arr", arrInt64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("int64s as ArrayValue", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Array("arr", int64s(arrInt64s)) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("float64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Float64s("arr", arrFloat64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("float64s as ArrayValue", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Array("arr", float64s(arrFloat64s)) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("strings", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Strings("arr", arrStrings) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("strings as ArrayValue", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Array("arr", strings(arrStrings)) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) -} diff --git a/log/benchmarks/fields/go.mod b/log/benchmarks/fields/go.mod deleted file mode 100644 index 482bd27a..00000000 --- a/log/benchmarks/fields/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module benchmark-fields - -go 1.24 diff --git a/log/benchmarks/fields/main.go b/log/benchmarks/fields/main.go deleted file mode 100644 index 2a883b79..00000000 --- a/log/benchmarks/fields/main.go +++ /dev/null @@ -1,19 +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 main - -func main() {} diff --git a/log/benchmarks/fields/main_test.go b/log/benchmarks/fields/main_test.go deleted file mode 100644 index 5f8ba54d..00000000 --- a/log/benchmarks/fields/main_test.go +++ /dev/null @@ -1,170 +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 main_test - -import ( - "bytes" - "testing" - - "benchmark-fields/encoder" - "benchmark-fields/field-value" - "benchmark-fields/value-interface" - "benchmark-fields/value-struct" -) - -var ( - arrBools = []bool{true, false, true, false, true, false} - arrInt64s = []int64{1, 2, 3, 4, 5, 6, 7, 8} - arrFloat64s = []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8} - arrStrings = []string{"a", "b", "c", "d", "e", "f", "g", "h"} -) - -func BenchmarkFields(b *testing.B) { - - // value_interface/bools-8 10864762 109.4 ns/op 152 B/op 4 allocs/op - // value_struct/bools-8 11207328 107.6 ns/op 152 B/op 4 allocs/op - // field_value/bools-8 11133696 108.8 ns/op 152 B/op 4 allocs/op - - // value_interface/int64s-8 8831475 138.4 ns/op 152 B/op 4 allocs/op - // value_struct/int64s-8 8929010 134.1 ns/op 152 B/op 4 allocs/op - // field_value/int64s-8 8978941 132.0 ns/op 152 B/op 4 allocs/op - - // value_interface/float64s-8 1927635 614.1 ns/op 344 B/op 12 allocs/op - // value_struct/float64s-8 1980488 604.1 ns/op 344 B/op 12 allocs/op - // field_value/float64s-8 1992417 601.2 ns/op 344 B/op 12 allocs/op - - // value_interface/strings-8 8276900 144.9 ns/op 152 B/op 4 allocs/op - // value_struct/strings-8 8107906 148.5 ns/op 152 B/op 4 allocs/op - // field_value/strings-8 8212352 149.6 ns/op 152 B/op 4 allocs/op - - b.Run("value_interface", func(b *testing.B) { - b.Run("bools", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := value_interface.Bools("arr", arrBools) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - b.Run("int64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := value_interface.Int64s("arr", arrInt64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - b.Run("float64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := value_interface.Float64s("arr", arrFloat64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - b.Run("strings", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := value_interface.Strings("arr", arrStrings) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - }) - - b.Run("value_struct", func(b *testing.B) { - b.Run("bools", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := value_struct.Bools("arr", arrBools) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - b.Run("int64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := value_struct.Int64s("arr", arrInt64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - b.Run("float64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := value_struct.Float64s("arr", arrFloat64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - b.Run("strings", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := value_struct.Strings("arr", arrStrings) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - }) - - b.Run("field_value", func(b *testing.B) { - b.Run("bools", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := field_value.Bools("arr", arrBools) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - b.Run("int64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := field_value.Int64s("arr", arrInt64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - b.Run("float64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := field_value.Float64s("arr", arrFloat64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - b.Run("strings", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := field_value.Strings("arr", arrStrings) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - }) -} diff --git a/log/benchmarks/fields/value-interface/field.go b/log/benchmarks/fields/value-interface/field.go deleted file mode 100644 index dd4527c3..00000000 --- a/log/benchmarks/fields/value-interface/field.go +++ /dev/null @@ -1,120 +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 value_interface - -import ( - "benchmark-fields/encoder" -) - -// 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. -} - -// 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)} -} - -// Int64 creates a Field for an int64 value. -func Int64(key string, val int64) Field { - return Field{Key: key, Val: Int64Value(val)} -} - -// Float64 creates a Field for a float64 value. -func Float64(key string, val float64) Field { - return Field{Key: key, Val: Float64Value(val)} -} - -// String creates a Field for a string value. -func String(key string, val string) Field { - return Field{Key: key, Val: StringValue(val)} -} - -// Reflect wraps any value into a Field using reflection. -func Reflect(key string, val interface{}) Field { - return Field{Key: key, Val: ReflectValue{Val: val}} -} - -// Bools creates a Field with a slice of booleans. -func Bools(key string, val []bool) Field { - return Field{Key: key, Val: BoolsValue(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)} -} - -// 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)} -} - -// 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)} -} - -// 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 Bools(key, val) - case int64: - return Int64(key, val) - case []int64: - return Int64s(key, val) - case float64: - return Float64(key, val) - case []float64: - return Float64s(key, val) - case string: - return String(key, val) - case []string: - return Strings(key, val) - default: - return Reflect(key, val) - } -} - -func (f *Field) Encode(enc encoder.Encoder) { - enc.AppendKey(f.Key) - f.Val.Encode(enc) -} diff --git a/log/benchmarks/fields/value-interface/field_test.go b/log/benchmarks/fields/value-interface/field_test.go deleted file mode 100644 index 9f439cb2..00000000 --- a/log/benchmarks/fields/value-interface/field_test.go +++ /dev/null @@ -1,77 +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 value_interface - -import ( - "bytes" - "testing" - - "benchmark-fields/encoder" -) - -func BenchmarkValueInterface(b *testing.B) { - - // bools-8 10998112 105.1 ns/op 152 B/op 4 allocs/op - // int64s-8 9017383 131.5 ns/op 152 B/op 4 allocs/op - // float64s-8 1904684 634.8 ns/op 344 B/op 12 allocs/op - // strings-8 8188070 145.1 ns/op 152 B/op 4 allocs/op - - arrBools := []bool{true, false, true, false, true, false} - arrInt64s := []int64{1, 2, 3, 4, 5, 6, 7, 8} - arrFloat64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8} - arrStrings := []string{"a", "b", "c", "d", "e", "f", "g", "h"} - - b.Run("bools", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Bools("arr", arrBools) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("int64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Int64s("arr", arrInt64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("float64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Float64s("arr", arrFloat64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("strings", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Strings("arr", arrStrings) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) -} diff --git a/log/benchmarks/fields/value-interface/value.go b/log/benchmarks/fields/value-interface/value.go deleted file mode 100644 index cd752f12..00000000 --- a/log/benchmarks/fields/value-interface/value.go +++ /dev/null @@ -1,140 +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 value_interface - -import ( - "benchmark-fields/encoder" -) - -// Value is an interface for types that can encode themselves using an Encoder. -type Value interface { - Encode(enc encoder.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.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.Encoder) { - enc.AppendInt64(int64(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.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.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.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.Encoder) { - enc.AppendArrayBegin() - for _, val := range v { - enc.AppendBool(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.Encoder) { - enc.AppendArrayBegin() - for _, val := range v { - enc.AppendInt64(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.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.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.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.Encoder) { - enc.AppendObjectBegin() - for _, f := range v { - f.Encode(enc) - } - enc.AppendObjectEnd() -} diff --git a/log/benchmarks/fields/value-struct/field.go b/log/benchmarks/fields/value-struct/field.go deleted file mode 100644 index d9f2194d..00000000 --- a/log/benchmarks/fields/value-struct/field.go +++ /dev/null @@ -1,120 +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 value_struct - -import ( - "benchmark-fields/encoder" -) - -// 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. -} - -// 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)} -} - -// Int64 creates a Field for an int64 value. -func Int64(key string, val int64) Field { - return Field{Key: key, Val: Int64Value(val)} -} - -// Float64 creates a Field for a float64 value. -func Float64(key string, val float64) Field { - return Field{Key: key, Val: Float64Value(val)} -} - -// String creates a Field for a string value. -func String(key string, val string) Field { - return Field{Key: key, Val: StringValue(val)} -} - -// Reflect wraps any value into a Field using reflection. -func Reflect(key string, val interface{}) Field { - return Field{Key: key, Val: ReflectValue(val)} -} - -// Bools creates a Field with a slice of booleans. -func Bools(key string, val []bool) Field { - return Field{Key: key, Val: BoolsValue(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)} -} - -// 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)} -} - -// 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...)} -} - -// 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 Bools(key, val) - case int64: - return Int64(key, val) - case []int64: - return Int64s(key, val) - case float64: - return Float64(key, val) - case []float64: - return Float64s(key, val) - case string: - return String(key, val) - case []string: - return Strings(key, val) - default: - return Reflect(key, val) - } -} - -func (f *Field) Encode(enc encoder.Encoder) { - enc.AppendKey(f.Key) - f.Val.Encode(enc) -} diff --git a/log/benchmarks/fields/value-struct/field_test.go b/log/benchmarks/fields/value-struct/field_test.go deleted file mode 100644 index 0068b01a..00000000 --- a/log/benchmarks/fields/value-struct/field_test.go +++ /dev/null @@ -1,77 +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 value_struct - -import ( - "bytes" - "testing" - - "benchmark-fields/encoder" -) - -func BenchmarkValueStruct(b *testing.B) { - - // bools-8 11046146 107.2 ns/op 152 B/op 4 allocs/op - // int64s-8 8929279 131.9 ns/op 152 B/op 4 allocs/op - // float64s-8 2011627 612.2 ns/op 344 B/op 12 allocs/op - // strings-8 7775536 155.2 ns/op 152 B/op 4 allocs/op - - arrBools := []bool{true, false, true, false, true, false} - arrInt64s := []int64{1, 2, 3, 4, 5, 6, 7, 8} - arrFloat64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8} - arrStrings := []string{"a", "b", "c", "d", "e", "f", "g", "h"} - - b.Run("bools", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Bools("arr", arrBools) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("int64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Int64s("arr", arrInt64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("float64s", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Float64s("arr", arrFloat64s) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) - - b.Run("strings", func(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - - for b.Loop() { - v := Strings("arr", arrStrings) - v.Encode(encoder.NewJSONEncoder(bytes.NewBuffer(nil))) - } - }) -} diff --git a/log/benchmarks/fields/value-struct/value.go b/log/benchmarks/fields/value-struct/value.go deleted file mode 100644 index e607d1a4..00000000 --- a/log/benchmarks/fields/value-struct/value.go +++ /dev/null @@ -1,207 +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 value_struct - -import ( - "math" - "unsafe" - - "benchmark-fields/encoder" -) - -type ValueType int - -const ( - ValueTypeBool = ValueType(iota) - ValueTypeInt64 - ValueTypeFloat64 - ValueTypeString - ValueTypeReflect - ValueTypeBools - ValueTypeInt64s - ValueTypeFloat64s - ValueTypeStrings - ValueTypeArray - ValueTypeObject -) - -type Value struct { - Type ValueType - Num uint64 - Any any -} - -// BoolValue represents a bool carried by Field. -func BoolValue(v bool) Value { - if v { - return Value{ - Type: ValueTypeBool, - Num: 1, - } - } else { - return Value{ - Type: ValueTypeBool, - Num: 0, - } - } -} - -// Int64Value represents an int64 carried by Field. -func Int64Value(v int64) Value { - return Value{ - Type: ValueTypeInt64, - Num: uint64(v), - } -} - -// Float64Value represents a float64 carried by Field. -func Float64Value(v float64) Value { - return Value{ - Type: ValueTypeFloat64, - Num: math.Float64bits(v), - } -} - -// StringValue represents a string carried by Field. -func StringValue(v string) Value { - return Value{ - Type: ValueTypeString, - Num: uint64(len(v)), - Any: unsafe.StringData(v), - } -} - -// ReflectValue represents an interface{} carried by Field. -func ReflectValue(v any) Value { - return Value{ - Type: ValueTypeReflect, - Any: v, - } -} - -// BoolsValue represents a slice of bool carried by Field. -func BoolsValue(v []bool) Value { - return Value{ - Type: ValueTypeBools, - Any: v, - } -} - -// Int64sValue represents a slice of int64 carried by Field. -func Int64sValue(v []int64) Value { - return Value{ - Type: ValueTypeInt64s, - Any: v, - } -} - -// Float64sValue represents a slice of float64 carried by Field. -func Float64sValue(v []float64) Value { - return Value{ - Type: ValueTypeFloat64s, - Any: v, - } -} - -// StringsValue represents a slice of string carried by Field. -func StringsValue(v []string) Value { - return Value{ - Type: ValueTypeStrings, - Any: v, - } -} - -type arrVal []Value - -func (v arrVal) Encode(enc encoder.Encoder) { - enc.AppendArrayBegin() - for _, val := range v { - val.Encode(enc) - } - enc.AppendArrayEnd() -} - -// ArrayValue represents a slice of Value carried by Field. -func ArrayValue(val ...Value) Value { - return Value{ - Type: ValueTypeArray, - Any: arrVal(val), - } -} - -type objVal []Field - -func (v objVal) Encode(enc encoder.Encoder) { - enc.AppendObjectBegin() - for _, f := range v { - f.Encode(enc) - } - enc.AppendObjectEnd() -} - -// ObjectValue represents a slice of Field carried by Field. -func ObjectValue(fields ...Field) Value { - return Value{ - Type: ValueTypeObject, - Any: objVal(fields), - } -} - -// Encode encodes the Value to the encoder. -func (v Value) Encode(enc encoder.Encoder) { - switch v.Type { - case ValueTypeBool: - enc.AppendBool(v.Num != 0) - case ValueTypeInt64: - enc.AppendInt64(int64(v.Num)) - case ValueTypeFloat64: - enc.AppendFloat64(math.Float64frombits(v.Num)) - case ValueTypeString: - enc.AppendString(unsafe.String(v.Any.(*byte), v.Num)) - case ValueTypeReflect: - enc.AppendReflect(v.Any) - case ValueTypeBools: - enc.AppendArrayBegin() - for _, val := range v.Any.([]bool) { - enc.AppendBool(val) - } - enc.AppendArrayEnd() - case ValueTypeInt64s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]int64) { - enc.AppendInt64(val) - } - enc.AppendArrayEnd() - case ValueTypeFloat64s: - enc.AppendArrayBegin() - for _, val := range v.Any.([]float64) { - enc.AppendFloat64(val) - } - enc.AppendArrayEnd() - case ValueTypeStrings: - enc.AppendArrayBegin() - for _, val := range v.Any.([]string) { - enc.AppendString(val) - } - enc.AppendArrayEnd() - case ValueTypeArray: - v.Any.(arrVal).Encode(enc) - case ValueTypeObject: - v.Any.(objVal).Encode(enc) - default: // for linter - } -} diff --git a/log/field.go b/log/field.go deleted file mode 100644 index 4c9b79f3..00000000 --- a/log/field.go +++ /dev/null @@ -1,405 +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" - "math" - "unsafe" - - "github.com/go-spring/spring-core/util" -) - -const MsgKey = "msg" - -// ValueType represents the type of value stored in a Field. -type ValueType int - -const ( - ValueTypeBool = ValueType(iota) - ValueTypeInt64 - ValueTypeUint64 - ValueTypeFloat64 - ValueTypeString - ValueTypeReflect - ValueTypeArray - ValueTypeObject -) - -// Field represents a structured log field with a key and a value. -type Field struct { - Key string - Type ValueType - Num uint64 - Any any -} - -// 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...)) -} - -// 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 { - if val { - return Field{ - Key: key, - Type: ValueTypeBool, - Num: 1, - } - } - return Field{ - Key: key, - Type: ValueTypeBool, - Num: 0, - } -} - -// 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[T util.IntType](key string, val T) Field { - return Field{ - Key: key, - Type: ValueTypeInt64, - Num: uint64(val), - } -} - -// IntPtr creates a Field from a *int, or returns Nil if pointer is nil. -func IntPtr[T util.IntType](key string, val *T) Field { - if val == nil { - return Nil(key) - } - return Int(key, *val) -} - -// Uint creates a Field for an uint value. -func Uint[T util.UintType](key string, val T) Field { - return Field{ - Key: key, - Type: ValueTypeUint64, - Num: uint64(val), - } -} - -// UintPtr creates a Field from a *uint, or returns Nil if pointer is nil. -func UintPtr[T util.UintType](key string, val *T) Field { - if val == nil { - return Nil(key) - } - return Uint(key, *val) -} - -// Float creates a Field for a float32 value. -func Float[T util.FloatType](key string, val T) Field { - return Field{ - Key: key, - Type: ValueTypeFloat64, - Num: math.Float64bits(float64(val)), - } -} - -// FloatPtr creates a Field from a *float32, or returns Nil if pointer is nil. -func FloatPtr[T util.FloatType](key string, val *T) Field { - if val == nil { - return Nil(key) - } - return Float(key, *val) -} - -// String creates a Field for a string value. -func String(key string, val string) Field { - return Field{ - Key: key, - Type: ValueTypeString, - Num: uint64(len(val)), // Store the length of the string - Any: unsafe.StringData(val), // Store the pointer to string data - } -} - -// 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) -} - -// Reflect wraps any value into a Field using reflection. -func Reflect(key string, val any) Field { - return Field{ - Key: key, - Type: ValueTypeReflect, - Any: val, - } -} - -type bools []bool - -// EncodeArray encodes a slice of bools using the Encoder interface. -func (arr bools) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendBool(v) - } -} - -// Bools creates a Field with a slice of booleans. -func Bools(key string, val []bool) Field { - return Array(key, bools(val)) -} - -type sliceOfInt[T util.IntType] []T - -// EncodeArray encodes a slice of ints using the Encoder interface. -func (arr sliceOfInt[T]) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendInt64(int64(v)) - } -} - -// Ints creates a Field with a slice of integers. -func Ints[T util.IntType](key string, val []T) Field { - return Array(key, sliceOfInt[T](val)) -} - -type sliceOfUint[T util.UintType] []T - -// EncodeArray encodes a slice of uints using the Encoder interface. -func (arr sliceOfUint[T]) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendUint64(uint64(v)) - } -} - -// Uints creates a Field with a slice of unsigned integers. -func Uints[T util.UintType](key string, val []T) Field { - return Array(key, sliceOfUint[T](val)) -} - -type sliceOfFloat[T util.FloatType] []T - -// EncodeArray encodes a slice of float32s using the Encoder interface. -func (arr sliceOfFloat[T]) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendFloat64(float64(v)) - } -} - -// Floats creates a Field with a slice of float32 values. -func Floats[T util.FloatType](key string, val []T) Field { - return Array(key, sliceOfFloat[T](val)) -} - -type sliceOfString []string - -// EncodeArray encodes a slice of strings using the Encoder interface. -func (arr sliceOfString) EncodeArray(enc Encoder) { - for _, v := range arr { - enc.AppendString(v) - } -} - -// Strings creates a Field with a slice of strings. -func Strings(key string, val []string) Field { - return Array(key, sliceOfString(val)) -} - -// ArrayValue is an interface for types that can be encoded as array. -type ArrayValue interface { - EncodeArray(enc Encoder) -} - -// Array creates a Field with array type, using the ArrayValue interface. -func Array(key string, val ArrayValue) Field { - return Field{ - Key: key, - Type: ValueTypeArray, - Any: 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, - Type: ValueTypeObject, - Any: fields, - } -} - -// 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 any) 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 Int(key, val) - case *int8: - return IntPtr(key, val) - case []int8: - return Ints(key, val) - - case int16: - return Int(key, val) - case *int16: - return IntPtr(key, val) - case []int16: - return Ints(key, val) - - case int32: - return Int(key, val) - case *int32: - return IntPtr(key, val) - case []int32: - return Ints(key, val) - - case int64: - return Int(key, val) - case *int64: - return IntPtr(key, val) - case []int64: - return Ints(key, val) - - case uint: - return Uint(key, val) - case *uint: - return UintPtr(key, val) - case []uint: - return Uints(key, val) - - case uint8: - return Uint(key, val) - case *uint8: - return UintPtr(key, val) - case []uint8: - return Uints(key, val) - - case uint16: - return Uint(key, val) - case *uint16: - return UintPtr(key, val) - case []uint16: - return Uints(key, val) - - case uint32: - return Uint(key, val) - case *uint32: - return UintPtr(key, val) - case []uint32: - return Uints(key, val) - - case uint64: - return Uint(key, val) - case *uint64: - return UintPtr(key, val) - case []uint64: - return Uints(key, val) - - case float32: - return Float(key, val) - case *float32: - return FloatPtr(key, val) - case []float32: - return Floats(key, val) - - case float64: - return Float(key, val) - case *float64: - return FloatPtr(key, val) - case []float64: - return Floats(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) - } -} - -// Encode encodes the Field into the Encoder based on its type. -func (f *Field) Encode(enc Encoder) { - enc.AppendKey(f.Key) - switch f.Type { - case ValueTypeBool: - enc.AppendBool(f.Num != 0) - case ValueTypeInt64: - enc.AppendInt64(int64(f.Num)) - case ValueTypeUint64: - enc.AppendUint64(f.Num) - case ValueTypeFloat64: - enc.AppendFloat64(math.Float64frombits(f.Num)) - case ValueTypeString: - enc.AppendString(unsafe.String(f.Any.(*byte), f.Num)) - case ValueTypeReflect: - enc.AppendReflect(f.Any) - case ValueTypeArray: - enc.AppendArrayBegin() - f.Any.(ArrayValue).EncodeArray(enc) - enc.AppendArrayEnd() - case ValueTypeObject: - enc.AppendObjectBegin() - WriteFields(enc, f.Any.([]Field)) - enc.AppendObjectEnd() - default: // for linter - } -} - -// WriteFields writes a slice of Field objects to the encoder. -func WriteFields(enc Encoder, fields []Field) { - for _, f := range fields { - f.Encode(enc) - } -} diff --git a/log/field_encoder.go b/log/field_encoder.go deleted file mode 100644 index 99d4e68c..00000000 --- a/log/field_encoder.go +++ /dev/null @@ -1,381 +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 any) -} - -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('"') - WriteLogString(enc.buf, 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('"') - WriteLogString(enc.buf, v) - enc.buf.WriteByte('"') -} - -// AppendReflect marshals any Go value into JSON and appends it. -func (enc *JSONEncoder) AppendReflect(v any) { - enc.appendSeparator() - enc.last = jsonTokenValue - b, err := json.Marshal(v) - if err != nil { - enc.buf.WriteByte('"') - WriteLogString(enc.buf, err.Error()) - enc.buf.WriteByte('"') - return - } - enc.buf.Write(b) -} - -// 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 - wroteField 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.wroteField { - enc.buf.WriteString(enc.separator) - } else { - enc.wroteField = true - } - WriteLogString(enc.buf, 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 - } - WriteLogString(enc.buf, v) -} - -// AppendReflect uses reflection to marshal any value as JSON. -// If nested, delegates to JSON encoder. -func (enc *TextEncoder) AppendReflect(v any) { - if enc.jsonDepth > 0 { - enc.jsonEncoder.AppendReflect(v) - return - } - b, err := json.Marshal(v) - if err != nil { - WriteLogString(enc.buf, err.Error()) - return - } - enc.buf.Write(b) -} - -/************************************* string ********************************/ - -// WriteLogString escapes and writes a string according to JSON rules. -func WriteLogString(buf *bytes.Buffer, s string) { - for i := 0; i < len(s); { - // Try to add a single-byte (ASCII) character directly - if tryAddRuneSelf(buf, s[i]) { - i++ - continue - } - // Decode multi-byte UTF-8 character - r, size := utf8.DecodeRuneInString(s[i:]) - // Handle invalid UTF-8 encoding - if tryAddRuneError(buf, r, size) { - i++ - continue - } - // Valid multi-byte rune; add as is - buf.WriteString(s[i : i+size]) - i += size - } -} - -// tryAddRuneSelf handles ASCII characters and escapes control/quote characters. -func tryAddRuneSelf(buf *bytes.Buffer, b byte) bool { - const _hex = "0123456789abcdef" - if b >= utf8.RuneSelf { - return false // not a single-byte rune - } - if 0x20 <= b && b != '\\' && b != '"' { - buf.WriteByte(b) - return true - } - // Handle escaping - switch b { - case '\\', '"': - buf.WriteByte('\\') - buf.WriteByte(b) - case '\n': - buf.WriteByte('\\') - buf.WriteByte('n') - case '\r': - buf.WriteByte('\\') - buf.WriteByte('r') - case '\t': - buf.WriteByte('\\') - buf.WriteByte('t') - default: - // Encode bytes < 0x20, except for the escape sequences above. - buf.WriteString(`\u00`) - buf.WriteByte(_hex[b>>4]) - buf.WriteByte(_hex[b&0xF]) - } - return true -} - -// tryAddRuneError checks and escapes invalid UTF-8 runes. -func tryAddRuneError(buf *bytes.Buffer, r rune, size int) bool { - if r == utf8.RuneError && size == 1 { - buf.WriteString(`\ufffd`) - return true - } - return false -} diff --git a/log/field_test.go b/log/field_test.go deleted file mode 100644 index 053e5cb5..00000000 --- a/log/field_test.go +++ /dev/null @@ -1,287 +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/go-spring/spring-core/util" - "github.com/lvan100/go-assert" -) - -var testFields = []Field{ - Msgf("hello %s", "中国"), - Msg("hello world\n\\\t\"\r"), - Any("null", nil), - Any("bool", false), - Any("bool_ptr", util.Ptr(true)), - Any("bool_ptr_nil", (*bool)(nil)), - Any("bools", []bool{true, true, false}), - Any("int", int(1)), - Any("int_ptr", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.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", util.Ptr("a")), - Any("string_ptr_nil", (*string)(nil)), - Any("string_slice", []string{"a", "b", "c"}), - Object("object", Any("int64", int64(1)), Any("uint64", uint64(1)), Any("string", "a")), - Reflect("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": true, - "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" - ], - "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=true||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"]||` + - `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/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 8419bcc9..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 range 2 { - 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 b.Loop() { - internal.Caller(0, true) - } - }) - - b.Run("slow", func(b *testing.B) { - for b.Loop() { - internal.Caller(0, false) - } - }) -} diff --git a/log/log.go b/log/log.go deleted file mode 100644 index 3bb99cf8..00000000 --- a/log/log.go +++ /dev/null @@ -1,171 +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" - "github.com/go-spring/spring-core/util" -) - -var ( - TagGS = GetTag("_gs") - TagDef = GetTag("_def") - TagBiz = GetTag("_biz") -) - -// 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 - -// Fields converts a map of string keys to any values to a slice of Field. -func Fields(fields map[string]any) []Field { - var ret []Field - for _, k := range util.OrderedMapKeys(fields) { - ret = append(ret, Any(k, fields[k])) - } - return ret -} - -// 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, 2, fn()...) - } -} - -// Tracef logs a message at TraceLevel using a tag and a formatted message. -func Tracef(ctx context.Context, tag *Tag, format string, args ...any) { - if tag.GetLogger().EnableLevel(TraceLevel) { - Record(ctx, TraceLevel, tag, 2, Msgf(format, args...)) - } -} - -// 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, 2, fn()...) - } -} - -// Debugf logs a message at DebugLevel using a tag and a formatted message. -func Debugf(ctx context.Context, tag *Tag, format string, args ...any) { - if tag.GetLogger().EnableLevel(DebugLevel) { - Record(ctx, DebugLevel, tag, 2, Msgf(format, args...)) - } -} - -// Info logs a message at InfoLevel using structured fields. -func Info(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, InfoLevel, tag, 2, fields...) -} - -// Infof logs a message at InfoLevel using a formatted message. -func Infof(ctx context.Context, tag *Tag, format string, args ...any) { - Record(ctx, InfoLevel, tag, 2, Msgf(format, args...)) -} - -// Warn logs a message at WarnLevel using structured fields. -func Warn(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, WarnLevel, tag, 2, fields...) -} - -// Warnf logs a message at WarnLevel using a formatted message. -func Warnf(ctx context.Context, tag *Tag, format string, args ...any) { - Record(ctx, WarnLevel, tag, 2, Msgf(format, args...)) -} - -// Error logs a message at ErrorLevel using structured fields. -func Error(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, ErrorLevel, tag, 2, fields...) -} - -// Errorf logs a message at ErrorLevel using a formatted message. -func Errorf(ctx context.Context, tag *Tag, format string, args ...any) { - Record(ctx, ErrorLevel, tag, 2, Msgf(format, args...)) -} - -// Panic logs a message at PanicLevel using structured fields. -func Panic(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, PanicLevel, tag, 2, fields...) -} - -// Panicf logs a message at PanicLevel using a formatted message. -func Panicf(ctx context.Context, tag *Tag, format string, args ...any) { - Record(ctx, PanicLevel, tag, 2, Msgf(format, args...)) -} - -// Fatal logs a message at FatalLevel using structured fields. -func Fatal(ctx context.Context, tag *Tag, fields ...Field) { - Record(ctx, FatalLevel, tag, 2, fields...) -} - -// Fatalf logs a message at FatalLevel using a formatted message. -func Fatalf(ctx context.Context, tag *Tag, format string, args ...any) { - Record(ctx, FatalLevel, tag, 2, Msgf(format, args...)) -} - -// 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, skip int, fields ...Field) { - var l *Logger - - // Check if the logger is enabled for the given level - if l = tag.GetLogger(); !l.EnableLevel(level) { - return - } - - file, line := internal.Caller(skip, 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 - - l.Publish(e) -} diff --git a/log/log_event.go b/log/log_event.go deleted file mode 100644 index 65fef551..00000000 --- a/log/log_event.go +++ /dev/null @@ -1,64 +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) -} - -// Reset clears the Event's fields so the instance can be reused. -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 -} - -// GetEvent retrieves an *Event from the pool. -// If the pool is empty, a new Event will be created by the New function. -func GetEvent() *Event { - return eventPool.Get().(*Event) -} - -// PutEvent resets the given Event and returns it to the pool for reuse. -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 a5f3f2bb..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 (l Level) String() string { - switch l { - 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(s string) (Level, error) { - switch strings.ToUpper(s) { - 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", s) - } -} diff --git a/log/log_level_test.go b/log/log_level_test.go deleted file mode 100644 index 7f638ac0..00000000 --- a/log/log_level_test.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 ( - "fmt" - "strings" - "testing" - - "github.com/lvan100/go-assert" -) - -func TestParseLevel(t *testing.T) { - tests := []struct { - str string - want Level - wantErr error - }{ - { - str: "none", - want: NoneLevel, - }, - { - str: "trace", - want: TraceLevel, - }, - { - str: "debug", - want: DebugLevel, - }, - { - str: "info", - want: InfoLevel, - }, - { - str: "warn", - want: WarnLevel, - }, - { - str: "error", - want: ErrorLevel, - }, - { - str: "panic", - want: PanicLevel, - }, - { - str: "fatal", - want: FatalLevel, - }, - { - str: "unknown", - want: Level(-1), - wantErr: fmt.Errorf("invalid level unknown"), - }, - } - for _, tt := range tests { - got, err := ParseLevel(tt.str) - assert.That(t, got).Equal(tt.want) - assert.That(t, err).Equal(tt.wantErr) - if tt.str == "unknown" { - assert.ThatString(t, got.String()).Equal("INVALID") - } else { - assert.ThatString(t, got.String()).Equal(strings.ToUpper(tt.str)) - } - } -} diff --git a/log/log_reader.go b/log/log_reader.go deleted file mode 100644 index ede887c6..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 range indent { - 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: // for linter - } - } - 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 ee4d2025..00000000 --- a/log/log_refresh.go +++ /dev/null @@ -1,241 +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 - } - // nolint: errcheck - defer 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: 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: Properties section must be unique") - } - if len(nodes) == 1 { - for _, c := range nodes[0].Children { - if c.Label != "Property" { - continue - } - name, ok := c.Attributes["name"] - if !ok { - return fmt.Errorf("RefreshReader: attribute 'name' not found for node %s", c.Label) - } - properties[name] = c.Text - } - } - - // Parse section - nodes = rootNode.getChildren("Appenders") - if len(nodes) == 0 { - return errors.New("RefreshReader: Appenders section not found") - } - if len(nodes) > 1 { - return errors.New("RefreshReader: Appenders 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: Loggers section not found") - } - if len(nodes) > 1 { - return errors.New("RefreshReader: Loggers 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"] = "::root::" - } - - 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 fmt.Errorf("RefreshReader: attribute 'name' not found for node %s", c.Label) - } - 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 - default: // for linter - } - - 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 attribute 'tags'") - } - } else { - var ss []string - for s := range strings.SplitSeq(base.Tags, ",") { - if s = strings.TrimSpace(s); s == "" { - continue - } - ss = append(ss, s) - } - if len(ss) == 0 { - return fmt.Errorf("RefreshReader: logger must have attribute 'tags'") - } - for _, s := range ss { - cTags[s] = logger - } - } - } - - if cRoot == nil { - return errors.New("RefreshReader: 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]) - } - - // todo(lvan100): currently, there is only one refresh operation, - // so exception handling is temporarily ignored. - - for _, a := range cAppenders { - if err := a.Start(); err != nil { - return errutil.WrapError(err, "RefreshReader: appender %s start error", a.GetName()) - } - } - for _, l := range cLoggers { - if err := l.Start(); err != nil { - return errutil.WrapError(err, "RefreshReader: logger %s start error", l.GetName()) - } - } - - 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_refresh_test.go b/log/log_refresh_test.go deleted file mode 100644 index 99faa50d..00000000 --- a/log/log_refresh_test.go +++ /dev/null @@ -1,500 +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" - "strings" - "testing" - - "github.com/lvan100/go-assert" -) - -type funcReader func(p []byte) (n int, err error) - -func (r funcReader) Read(p []byte) (n int, err error) { - return r(p) -} - -func TestRefresh(t *testing.T) { - t.Cleanup(func() { - for _, tag := range tagMap { - tag.SetLogger(initLogger) - } - }) - - t.Run("file not exist", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshFile("testdata/file-not-exist.xml") - assert.ThatError(t, err).Matches("open testdata/file-not-exist.xml") - }) - - t.Run("already refresh", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshFile("testdata/log.xml") - assert.Nil(t, err) - // ... - err = RefreshFile("testdata/log.xml") - assert.ThatError(t, err).Matches("RefreshReader: log refresh already done") - }) - - t.Run("unsupported file", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(nil, ".json") - assert.ThatError(t, err).Matches("RefreshReader: unsupported file type .json") - }) - - t.Run("read file error", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(funcReader(func(p []byte) (n int, err error) { - return 0, errors.New("read error") - }), ".xml") - assert.ThatError(t, err).Matches("read error") - }) - - t.Run("read node error - 1", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(""), ".xml") - assert.ThatError(t, err).Matches("invalid XML structure: missing root element") - }) - - t.Run("read node error - 2", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: Configuration root not found") - }) - - t.Run("more Properties", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: Properties section must be unique") - }) - - t.Run("error Property", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - abc - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: attribute 'name' not found for node Property") - }) - - t.Run("no Appenders", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: Appenders section not found") - }) - - t.Run("more Appenders", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: Appenders section must be unique") - }) - - t.Run("unfound Appender", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: plugin NotExistAppender not found") - }) - - t.Run("Appender error - 1", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: attribute 'name' not found") - }) - - t.Run("Appender error - 2", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("create plugin log.FileAppender error << found no plugin elements for struct field Layout") - }) - - t.Run("Appender error - 3", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("create plugin log.FileAppender error << found no attribute for struct field FileName") - }) - - t.Run("no Loggers", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: Loggers section not found") - }) - - t.Run("more Loggers", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: Loggers section must be unique") - }) - - t.Run("no Logger", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: found no root logger") - }) - - t.Run("Logger error - 1", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: plugin NotExistLogger not found") - }) - - t.Run("Logger error - 2", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: attribute 'name' not found for node Logger") - }) - - t.Run("Logger error - 3", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("create plugin log.LoggerConfig error << found no attribute for struct field Level") - }) - - t.Run("Logger error - 4", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: appender not-exist-appender not found") - }) - - t.Run("Logger error - 5", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: logger must have attribute 'tags'") - }) - - t.Run("Root error - 1", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: found no root logger") - }) - - t.Run("Root error - 2", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("create plugin log.LoggerConfig error << found no attribute for struct field Level") - }) - - t.Run("Root error - 3", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("create plugin log.LoggerConfig error << found no plugin elements for struct field AppenderRefs") - }) - - t.Run("Root error - 4", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: root logger can not have attribute 'tags'") - }) - - t.Run("Root error - 5", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: found more than one root loggers") - }) - - t.Run("tag error", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: `a\\[` regexp compile error << error parsing regexp: missing closing \\]: `\\[`") - }) - - t.Run("appender start error", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: appender file start error << open /not-exist-dir/access.log: no such file or directory") - }) - - t.Run("logger start error", func(t *testing.T) { - defer func() { initOnce.Store(false) }() - err := RefreshReader(strings.NewReader(` - - - - - - - - - - - - - - - - - `), ".xml") - assert.ThatError(t, err).Matches("RefreshReader: logger ::root:: start error << bufferSize is too small") - }) -} diff --git a/log/log_tag.go b/log/log_tag.go deleted file mode 100644 index 87615bd9..00000000 --- a/log/log_tag.go +++ /dev/null @@ -1,116 +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 ( - "slices" - "strings" - "sync/atomic" - - "github.com/go-spring/spring-core/util" -) - -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) -} - -// isValidTag checks whether the tag is valid according to the following rules: -// 1. The length must be between 3 and 36 characters. -// 2. Only lowercase letters (a-z), digits (0-9), and underscores (_) are allowed. -// 3. The tag can start with an underscore. -// 4. Underscores separate the tag into 1 to 4 non-empty segments. -// 5. No empty segments are allowed (i.e., no consecutive or trailing underscores). -func isValidTag(tag string) bool { - if len(tag) < 3 || len(tag) > 36 { - return false - } - for i := range len(tag) { - c := tag[i] - if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '_' { - return false - } - } - ss := strings.Split(strings.TrimPrefix(tag, "_"), "_") - if len(ss) < 1 || len(ss) > 4 { - return false - } - return !slices.Contains(ss, "") -} - -// 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 { - if !isValidTag(tag) { - panic("invalid tag name") - } - m, ok := tagMap[tag] - if !ok { - m = &Tag{s: tag} - m.v.Store(initLogger) - tagMap[tag] = m - } - return m -} - -// GetAllTags returns all registered tags. -func GetAllTags() []string { - return util.OrderedMapKeys(tagMap) -} diff --git a/log/log_tag_test.go b/log/log_tag_test.go deleted file mode 100644 index c861586f..00000000 --- a/log/log_tag_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 log - -import ( - "strings" - "testing" - - "github.com/lvan100/go-assert" -) - -func TestIsValidTag(t *testing.T) { - tests := []struct { - name string - tag string - want bool - }{ - {"valid_1segments", "_def", true}, - {"valid_4segments", "service_module_submodule_component00", true}, - {"too_short_2", "ab", false}, - {"too_long_37", strings.Repeat("a", 37), false}, - {"uppercase", "Invalid_Tag", false}, - {"special_char", "tag!name", false}, - {"space", "tag name", false}, - {"hyphen", "tag-name", false}, - {"too_many_segments", "a_b_c_d_e", false}, - {"leading_underscore_1", "_service_component", true}, - {"leading_underscore_2", "__service_component", false}, - {"trailing_underscore_1", "service_component_", false}, - {"trailing_underscore_2", "service_component__", false}, - {"consecutive_underscore", "service__component", false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := isValidTag(tt.tag); got != tt.want { - t.Errorf("isValidTag(%q) = %v, want %v", tt.tag, got, tt.want) - } - }) - } -} - -func TestGetAllTags(t *testing.T) { - tags := GetAllTags() - assert.That(t, tags).Equal([]string{ - "_biz", - "_com_request_in", - "_com_request_out", - "_def", - "_gs", - }) -} diff --git a/log/log_test.go b/log/log_test.go deleted file mode 100644 index 7b826d3a..00000000 --- a/log/log_test.go +++ /dev/null @@ -1,147 +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 ( - "bytes" - "context" - "os" - "strings" - "testing" - "time" - - "github.com/go-spring/spring-core/log" - "github.com/lvan100/go-assert" -) - -var ( - keyTraceID int - keySpanID int -) - -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() - - logBuf := bytes.NewBuffer(nil) - log.Stdout = logBuf - defer func() { - log.Stdout = os.Stdout - }() - - log.TimeNow = func(ctx context.Context) time.Time { - return time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC) - } - - log.StringFromContext = func(ctx context.Context) string { - return "" - } - - log.FieldsFromContext = func(ctx context.Context) []log.Field { - traceID, _ := ctx.Value(&keyTraceID).(string) - spanID, _ := ctx.Value(&keySpanID).(string) - return []log.Field{ - log.String("trace_id", traceID), - log.String("span_id", spanID), - } - } - - // not print - log.Debug(ctx, TagRequestOut, func() []log.Field { - return []log.Field{ - log.Msgf("hello %s", "world"), - } - }) - - // print - log.Info(ctx, TagDefault, log.Msgf("hello %s", "world")) - log.Info(ctx, TagRequestIn, log.Msgf("hello %s", "world")) - - err := log.RefreshFile("testdata/log.xml") - assert.Nil(t, err) - - ctx = context.WithValue(ctx, &keyTraceID, "0a882193682db71edd48044db54cae88") - ctx = context.WithValue(ctx, &keySpanID, "50ef0724418c0a66") - - // print - log.Trace(ctx, TagRequestOut, func() []log.Field { - return []log.Field{ - log.Msgf("hello %s", "world"), - } - }) - - // print - log.Debug(ctx, TagRequestOut, func() []log.Field { - return []log.Field{ - log.Msgf("hello %s", "world"), - } - }) - - // print - log.Info(ctx, TagRequestIn, log.Msgf("hello %s", "world")) - log.Warn(ctx, TagRequestIn, log.Msgf("hello %s", "world")) - log.Error(ctx, TagRequestIn, log.Msgf("hello %s", "world")) - log.Panic(ctx, TagRequestIn, log.Msgf("hello %s", "world")) - log.Fatal(ctx, TagRequestIn, log.Msgf("hello %s", "world")) - - // print - log.Infof(ctx, TagRequestIn, "hello %s", "world") - log.Warnf(ctx, TagRequestIn, "hello %s", "world") - log.Errorf(ctx, TagRequestIn, "hello %s", "world") - log.Panicf(ctx, TagRequestIn, "hello %s", "world") - log.Fatalf(ctx, TagRequestIn, "hello %s", "world") - - // not print - log.Info(ctx, TagDefault, log.Msgf("hello %s", "world")) - - // print - log.Warn(ctx, TagDefault, log.Msgf("hello %s", "world")) - log.Error(ctx, TagDefault, log.Msgf("hello %s", "world")) - log.Panic(ctx, TagDefault, log.Msgf("hello %s", "world")) - - // print - log.Error(ctx, TagDefault, log.Fields(map[string]any{ - "key1": "value1", - "key2": "value2", - })...) - - expectLog := ` -[INFO][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:74] _def||trace_id=||span_id=||msg=hello world -[INFO][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:75] _com_request_in||trace_id=||span_id=||msg=hello world -{"level":"trace","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:84","tag":"_com_request_out","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"debug","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:91","tag":"_com_request_out","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"info","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:98","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"warn","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:99","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"error","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:100","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"panic","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:101","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"fatal","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:102","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"info","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:105","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"warn","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:106","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"error","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:107","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"panic","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:108","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -{"level":"fatal","time":"2025-06-01T00:00:00.000","fileLine":"/Users/didi/spring-core/log/log_test.go:109","tag":"_com_request_in","trace_id":"0a882193682db71edd48044db54cae88","span_id":"50ef0724418c0a66","msg":"hello world"} -[WARN][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:115] _def||trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66||msg=hello world -[ERROR][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:116] _def||trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66||msg=hello world -[PANIC][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:117] _def||trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66||msg=hello world -[ERROR][2025-06-01T00:00:00.000][/Users/didi/spring-core/log/log_test.go:120] _def||trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66||key1=value1||key2=value2 -` - - assert.ThatString(t, logBuf.String()).Equal(strings.TrimLeft(expectLog, "\n")) -} diff --git a/log/plugin.go b/log/plugin.go deleted file mode 100644 index eaba24f0..00000000 --- a/log/plugin.go +++ /dev/null @@ -1,283 +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 any](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]() - if t.Kind() != reflect.Struct { - panic("T must be struct") - } - 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 := range v.NumField() { - 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 len(ss) != 2 { - return "", false - } else if ss[0] == key { - return ss[1], 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 := range len(children) { - 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 8b3bf925..00000000 --- a/log/plugin_appender.go +++ /dev/null @@ -1,102 +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 ( - "io" - "os" -) - -// Stdout is the standard output stream used by appenders. -var Stdout io.Writer = os.Stdout - -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 - GetName() string // Returns the appender name - 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) GetName() string { return c.Name } -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) - _, _ = Stdout.Write(data) -} - -// FileAppender writes formatted log events to a specified file. -type FileAppender struct { - BaseAppender - FileName string `PluginAttribute:"fileName"` - - file *os.File -} - -// Start opens the 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 83da3585..00000000 --- a/log/plugin_appender_test.go +++ /dev/null @@ -1,126 +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) - - Stdout = file - defer func() { - Stdout = os.Stdout - }() - - 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 c24b38bd..00000000 --- a/log/plugin_layout.go +++ /dev/null @@ -1,170 +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 { - 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 -} - -// 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) - - headers := make([]Field, 0, 5) - headers = append(headers, String("level", strings.ToLower(e.Level.String()))) - headers = append(headers, String("time", e.Time.Format("2006-01-02T15:04:05.000"))) - headers = append(headers, String("fileLine", c.GetFileLine(e))) - headers = append(headers, String("tag", e.Tag)) - - if e.CtxString != "" { - headers = append(headers, String("ctxString", e.CtxString)) - } - - enc := NewJSONEncoder(buf) - enc.AppendEncoderBegin() - WriteFields(enc, headers) - 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 a8421ae4..00000000 --- a/log/plugin_layout_test.go +++ /dev/null @@ -1,137 +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, - }, - } - 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") - }) -} - -func TestJSONLayout(t *testing.T) { - - t.Run("success", func(t *testing.T) { - layout := &JSONLayout{ - BaseLayout{ - FileLineLength: 48, - }, - } - 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") - }) -} diff --git a/log/plugin_logger.go b/log/plugin_logger.go deleted file mode 100644 index 26edb1a9..00000000 --- a/log/plugin_logger.go +++ /dev/null @@ -1,140 +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(logger string, e *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 - GetName() string // Get the name of the logger - 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 -} - -// 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"` -} - -// GetName returns the name of the logger. -func (c *baseLoggerConfig) GetName() string { - return c.Name -} - -// 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 -} - -func (c *LoggerConfig) Start() error { return nil } -func (c *LoggerConfig) Stop() {} - -// 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 - if OnDropEvent != nil { - OnDropEvent(c.Name, e) - } - // Return the event to the pool - PutEvent(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 5b528caa..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 range 5 { - 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(logger string, e *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 range 5000 { - 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 range 5 { - 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 8d1570d3..00000000 --- a/log/plugin_test.go +++ /dev/null @@ -1,273 +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[int]("DummyLayout", PluginTypeLayout) - }, "T must be struct") - assert.Panic(t, func() { - RegisterPlugin[FileAppender]("File", PluginTypeAppender) - }, "duplicate plugin Appender in .*/plugin_appender.go:30 and .*/plugin_test.go:31") -} - -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 - 1", 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("plugin not found - 2", func(t *testing.T) { - type ErrorPlugin struct { - Layout Layout `PluginElement:"Layout,default"` - } - 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("plugin not found - 3", func(t *testing.T) { - type ErrorPlugin struct { - Layout Layout `PluginElement:"Layout,default=DummyLayout"` - } - typ := reflect.TypeFor[ErrorPlugin]() - _, err := NewPlugin(typ, &Node{ - Children: []*Node{ - {Label: "File"}, - }, - }, nil) - assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << plugin DummyLayout not found for struct field Layout") - }) - - t.Run("NewPlugin error - 1", 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\"`) - }) - - t.Run("NewPlugin error - 2", func(t *testing.T) { - type ErrorPlugin struct { - Appender Appender `PluginElement:"Appender,default=File"` - } - typ := reflect.TypeFor[ErrorPlugin]() - _, err := NewPlugin(typ, &Node{}, nil) - assert.ThatError(t, err).Matches(`create plugin log.ErrorPlugin error << create plugin log.FileAppender error ` + - `<< found no attribute for struct field Name`) - }) - - t.Run("NewPlugin error - 3", func(t *testing.T) { - type ErrorPlugin struct { - Layout Layout `PluginElement:"Layout"` - } - typ := reflect.TypeFor[ErrorPlugin]() - _, err := NewPlugin(typ, &Node{ - Children: []*Node{ - {Label: "TextLayout"}, - {Label: "TextLayout"}, - }, - }, nil) - assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << found 2 plugin elements for struct field Layout") - }) - - t.Run("NewPlugin error - 4", func(t *testing.T) { - type ErrorPlugin struct { - Layout map[string]Layout `PluginElement:"Layout"` - } - typ := reflect.TypeFor[ErrorPlugin]() - _, err := NewPlugin(typ, &Node{ - Children: []*Node{ - {Label: "TextLayout"}, - {Label: "TextLayout"}, - }, - }, nil) - assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << unsupported inject type map\\[string]log.Layout for struct field Layout") - }) - - t.Run("NewPlugin success - 1", func(t *testing.T) { - type ErrorPlugin struct { - Layout Layout `PluginElement:"Layout"` - } - typ := reflect.TypeFor[ErrorPlugin]() - _, err := NewPlugin(typ, &Node{ - Children: []*Node{ - {Label: "TextLayout"}, - }, - }, nil) - assert.Nil(t, err) - }) - - t.Run("NewPlugin success - 2", func(t *testing.T) { - type ErrorPlugin struct { - Layout Layout `PluginElement:"Layout,default=TextLayout"` - } - typ := reflect.TypeFor[ErrorPlugin]() - _, err := NewPlugin(typ, &Node{}, nil) - assert.Nil(t, err) - }) - - t.Run("NewPlugin success - 3", func(t *testing.T) { - type ErrorPlugin struct { - Layouts []Layout `PluginElement:"Layout,default=TextLayout"` - } - typ := reflect.TypeFor[ErrorPlugin]() - _, err := NewPlugin(typ, &Node{}, nil) - assert.Nil(t, err) - }) -} diff --git a/log/testdata/log.xml b/log/testdata/log.xml deleted file mode 100644 index 8fd460a1..00000000 --- a/log/testdata/log.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - 100KB - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 7c74c633c8dd51406a3bfc9011d8bce0bd3d5aff Mon Sep 17 00:00:00 2001 From: lvan100 Date: Sat, 14 Jun 2025 14:48:13 +0800 Subject: [PATCH 18/18] docs(README): update test coverage badge --- README.md | 6 ++++-- README_CN.md | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4382255e..23f43907 100644 --- a/README.md +++ b/README.md @@ -4,11 +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 2f2bff98..3d41ddfa 100644 --- a/README_CN.md +++ b/README_CN.md @@ -4,11 +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)、自动配置和生命周期管理,