pax_global_header00006660000000000000000000000064146776442730014536gustar00rootroot0000000000000052 comment=8b596604be7f92c9c3693485af6f6cac465d92a9 golang-github-zitadel-logging-0.6.1/000077500000000000000000000000001467764427300173475ustar00rootroot00000000000000golang-github-zitadel-logging-0.6.1/.github/000077500000000000000000000000001467764427300207075ustar00rootroot00000000000000golang-github-zitadel-logging-0.6.1/.github/workflows/000077500000000000000000000000001467764427300227445ustar00rootroot00000000000000golang-github-zitadel-logging-0.6.1/.github/workflows/release.yml000066400000000000000000000016671467764427300251210ustar00rootroot00000000000000name: Release on: push: branches: - main tags-ignore: - '**' pull_request: branches: - '**' workflow_dispatch: jobs: test: runs-on: ubuntu-22.04 strategy: matrix: go: ['1.21', '1.22'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v3 - name: Setup go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - run: go test -race ./... release: runs-on: ubuntu-22.04 needs: [test] if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Source checkout uses: actions/checkout@v3 - name: Semantic Release uses: cycjimmy/semantic-release-action@v2 with: dry_run: false semantic_version: 17.0.4 extra_plugins: | @semantic-release/exec@5.0.0 golang-github-zitadel-logging-0.6.1/.gitignore000066400000000000000000000004151467764427300213370ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ golang-github-zitadel-logging-0.6.1/.releaserc.js000066400000000000000000000003101467764427300217220ustar00rootroot00000000000000module.exports = { branches: ["main"], plugins: [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/github" ] }; golang-github-zitadel-logging-0.6.1/LICENSE000066400000000000000000000261351467764427300203630ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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 http://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. golang-github-zitadel-logging-0.6.1/README.md000066400000000000000000000032221467764427300206250ustar00rootroot00000000000000# logging [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Release](https://github.com/zitadel/logging/workflows/Release/badge.svg)](https://github.com/zitadel/logging/actions) [![license](https://badgen.net/github/license/zitadel/logging/)](https://github.com/zitadel/logging/blob/master/LICENSE) [![release](https://badgen.net/github/release/zitadel/logging/stable)](https://github.com/zitadel/logging/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/logging)](https://goreportcard.com/report/github.com/zitadel/logging) > This project is in alpha state. It can AND will continue breaking until version 1.0.0 is released ## What Is It TBD ## Supported Go Versions For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:). Versions that also build are marked with :warning:. | Version | Supported | |---------|--------------------| | <1.21 | :x: | | 1.21 | :white_check_mark: | | 1.22 | :white_check_mark: | ## License The full functionality of this library is and stays open source and free to use for everyone. Visit our [website](https://zitadel.ch) and get in touch. See the exact licensing terms [here](./LICENSE) 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. golang-github-zitadel-logging-0.6.1/attributes.go000066400000000000000000000025751467764427300220750ustar00rootroot00000000000000package logging import ( "fmt" "net/http" "log/slog" ) // StringValuer returns a Valuer that // forces the logger to use the type's String // method, even in json ouput mode. // By wrapping the type we defer String // being called to the point we actually log. func StringerValuer(s fmt.Stringer) slog.LogValuer { return stringerValuer{s} } type stringerValuer struct { fmt.Stringer } func (v stringerValuer) LogValue() slog.Value { return slog.StringValue(v.String()) } func requestToAttr(req *http.Request) slog.Attr { return slog.Group("request", slog.String("method", req.Method), slog.Any("url", StringerValuer(req.URL)), ) } func responseToAttr(resp *http.Response) slog.Attr { return slog.Group("response", slog.String("status", resp.Status), slog.Int64("content_length", resp.ContentLength), ) } // LoggedWriter stores information regarding the response. // This might be status code, amount of data written or header. type LoggedWriter interface { http.ResponseWriter // Attr is called after the next handler // in the Middleware returns and // the complete reponse should have been written. // // The returned Attribute should be a [slog.Group] // containing response Attributes. Attr() slog.Attr // Err() is called by the middleware to check // if the underlying writer returned an error. // If so, the middleware will print an ERROR line. Err() error } golang-github-zitadel-logging-0.6.1/attributes_test.go000066400000000000000000000006571467764427300231330ustar00rootroot00000000000000package logging import ( "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func Test_requestToAttr(t *testing.T) { out, logger := newTestLogger() logger.Info("test", requestToAttr( httptest.NewRequest("GET", "/taget", nil), )) want := `{ "level":"INFO", "msg":"test", "time":"not", "request":{ "method":"GET", "url":"/taget" } }` got := out.String() assert.JSONEq(t, want, got) } golang-github-zitadel-logging-0.6.1/config.go000066400000000000000000000063021467764427300211440ustar00rootroot00000000000000package logging import ( "encoding/json" "fmt" "log/slog" "os" "github.com/sirupsen/logrus" ) type Config struct { Level string `json:"level"` Formatter formatter `json:"formatter"` LocalLogger bool `json:"localLogger"` AddSource bool `json:"addSource"` } type formatter struct { Format string `json:"format"` Data map[string]interface{} `json:"data"` } type loggingConfig Config func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { log = (*logger)(logrus.New()) err := unmarshal((*loggingConfig)(c)) if err != nil { return err } return c.SetLogger() } func (c *Config) UnmarshalJSON(data []byte) error { log = (*logger)(logrus.New()) err := json.Unmarshal(data, (*loggingConfig)(c)) if err != nil { return err } return c.SetLogger() } func (c *Config) SetLogger() (err error) { err = c.parseFormatter() if err != nil { return err } err = c.parseLevel() if err != nil { return err } err = c.unmarshalFormatter() if err != nil { return err } c.setGlobal() return nil } func (c *Config) setGlobal() { if c.LocalLogger { return } logrus.SetFormatter(log.Formatter) logrus.SetLevel(log.Level) logrus.SetReportCaller(log.ReportCaller) log = (*logger)(logrus.StandardLogger()) } func (c *Config) unmarshalFormatter() error { formatterData, err := json.Marshal(c.Formatter.Data) if err != nil { return err } return json.Unmarshal(formatterData, log.Formatter) } func (c *Config) parseLevel() error { if c.Level == "" { log.Level = logrus.InfoLevel return nil } level, err := logrus.ParseLevel(c.Level) if err != nil { return err } log.Level = level return nil } const ( FormatterText = "text" FormatterJSON = "json" ) func (c *Config) parseFormatter() error { switch c.Formatter.Format { case FormatterJSON: log.Formatter = &logrus.JSONFormatter{} case FormatterText, "": log.Formatter = &logrus.TextFormatter{} default: return fmt.Errorf("%s formatter not supported", c.Formatter) } return nil } // Slog constructs a slog.Logger with the Formatter and Level from config. func (c *Config) Slog() *slog.Logger { logger := slog.Default() var level slog.Level if err := level.UnmarshalText([]byte(c.Level)); err != nil { logger.Warn("invalid config, using default slog", "err", err) return logger } opts := &slog.HandlerOptions{ AddSource: c.AddSource, Level: level, ReplaceAttr: c.fieldMapToPlaceKey(), } switch c.Formatter.Format { case FormatterText: return slog.New(slog.NewTextHandler(os.Stderr, opts)) case FormatterJSON: return slog.New(slog.NewJSONHandler(os.Stderr, opts)) case "": logger.Warn("no slog format in config, using text handler") default: logger.Warn("unknown slog format in config, using text handler", "format", c.Formatter.Format) } return slog.New(slog.NewTextHandler(os.Stderr, opts)) } func (c *Config) fieldMapToPlaceKey() func(groups []string, a slog.Attr) slog.Attr { fieldMap, ok := c.Formatter.Data["fieldmap"].(map[string]interface{}) if !ok { return nil } return func(groups []string, a slog.Attr) slog.Attr { for key, newKey := range fieldMap { if a.Key == key { a.Key = newKey.(string) return a } } return a } } golang-github-zitadel-logging-0.6.1/config_test.go000066400000000000000000000061151467764427300222050ustar00rootroot00000000000000package logging import ( "encoding/json" "reflect" "testing" "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" ) func TestUnmarshalJSON(t *testing.T) { type expected struct { wantErr bool level logrus.Level formatter logrus.Formatter } tests := [...]struct { name string jsonRaw []byte expect expected }{ { "debug level json format", []byte(`{"level": "debug", "formatter":{"format": "json", "data": {"dataKey":"hodor"}}}`), expected{false, logrus.DebugLevel, &logrus.JSONFormatter{}}, }, { "warn level text format", []byte(`{"level": "warn", "formatter":{"format": "text", "data": null}}`), expected{false, logrus.WarnLevel, &logrus.TextFormatter{}}, }, { "warn level text format forceColor", []byte(`{"level": "warn", "formatter":{"format": "text", "data": {"forceColors": true}}}`), expected{false, logrus.WarnLevel, &logrus.TextFormatter{ForceColors: true}}, }, { "warn level default format", []byte(`{"level": "error"}`), expected{false, logrus.ErrorLevel, &logrus.TextFormatter{}}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { config := Config{} err := json.Unmarshal(test.jsonRaw, &config) if !test.expect.wantErr && err != nil { t.Fatalf("no error expected: %s", err) } if log.Level != test.expect.level { t.Errorf("expected level \"%s\" got \"%s\"", test.expect.level, config.Level) } formatterType := reflect.TypeOf(log.Formatter).Elem() expectedType := reflect.TypeOf(test.expect.formatter).Elem() if formatterType.String() != expectedType.String() { t.Errorf("expected formatter \"%s\" got \"%s\"", expectedType, formatterType) } }) } } func TestUnmarshalYAML(t *testing.T) { type expected struct { wantErr bool level logrus.Level formatter logrus.Formatter } tests := [...]struct { name string yamlRaw []byte expect expected }{ { "debug level json format", []byte(` level: debug formatter: format: json data: dataKey: 'hodor' `), expected{false, logrus.DebugLevel, &logrus.JSONFormatter{DataKey: "hodor"}}, }, { "warn level text format", []byte(` level: warn formatter: format: text `), expected{false, logrus.WarnLevel, &logrus.TextFormatter{}}, }, { "warn level text format forceColor", []byte(` level: warn formatter: format: text data: forceColors: true `), expected{false, logrus.WarnLevel, &logrus.TextFormatter{ForceColors: true}}, }, { "warn level default format", []byte(`level: error`), expected{false, logrus.ErrorLevel, &logrus.TextFormatter{}}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { c := Config{} err := yaml.Unmarshal(test.yamlRaw, &c) if !test.expect.wantErr && err != nil { t.Fatalf("no error expected: %s", err) } if log.Level != test.expect.level { t.Errorf("expected level \"%s\" got \"%s\"", test.expect.level, log.Level) } if !reflect.DeepEqual(log.Formatter, test.expect.formatter) { t.Errorf("expected formatter \"%v\" got \"%v\"", test.expect.formatter, log.Formatter) } }) } } golang-github-zitadel-logging-0.6.1/context.go000066400000000000000000000007641467764427300213710ustar00rootroot00000000000000package logging import ( "context" "log/slog" ) type ctxKeyType struct{} var ctxKey ctxKeyType // FromContext takes a Logger from the context, if it was // previously set by [ToContext] func FromContext(ctx context.Context) (logger *slog.Logger, ok bool) { logger, ok = ctx.Value(ctxKey).(*slog.Logger) return logger, ok } // ToContext sets a Logger to the context. func ToContext(ctx context.Context, logger *slog.Logger) context.Context { return context.WithValue(ctx, ctxKey, logger) } golang-github-zitadel-logging-0.6.1/context_test.go000066400000000000000000000005641467764427300224260ustar00rootroot00000000000000package logging import ( "context" "testing" "log/slog" "github.com/stretchr/testify/assert" ) func TestContext(t *testing.T) { got, ok := FromContext(context.Background()) assert.False(t, ok) assert.Nil(t, got) want := slog.Default() ctx := ToContext(context.Background(), want) got, ok = FromContext(ctx) assert.True(t, ok) assert.Equal(t, want, got) } golang-github-zitadel-logging-0.6.1/go.mod000066400000000000000000000006741467764427300204640ustar00rootroot00000000000000module github.com/zitadel/logging go 1.21 require ( github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 gopkg.in/yaml.v2 v2.2.8 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.11.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.0 // indirect ) golang-github-zitadel-logging-0.6.1/go.sum000066400000000000000000000044421467764427300205060ustar00rootroot00000000000000github.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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-zitadel-logging-0.6.1/http_client.go000066400000000000000000000055631467764427300222240ustar00rootroot00000000000000package logging import ( "context" "net/http" "time" "log/slog" ) type ClientLoggerOption func(*logRountTripper) // WithFallbackLogger uses the passed logger if none was // found in the context. func WithFallbackLogger(logger *slog.Logger) ClientLoggerOption { return func(lrt *logRountTripper) { lrt.fallback = logger } } // WithClientDurationFunc allows overiding the request duration // for testing. func WithClientDurationFunc(df func(time.Time) time.Duration) ClientLoggerOption { return func(lrt *logRountTripper) { lrt.duration = df } } // WithClientGroup groups the log attributes // produced by the client. func WithClientGroup(name string) ClientLoggerOption { return func(lrt *logRountTripper) { lrt.group = name } } // WithClientRequestAttr allows customizing the information used // from a request as request attributes. func WithClientRequestAttr(requestToAttr func(*http.Request) slog.Attr) ClientLoggerOption { return func(lrt *logRountTripper) { lrt.reqToAttr = requestToAttr } } // WithClientResponseAttr allows customizing the information used // from a response as response attributes. func WithClientResponseAttr(responseToAttr func(*http.Response) slog.Attr) ClientLoggerOption { return func(lrt *logRountTripper) { lrt.resToAttr = responseToAttr } } // EnableHTTPClient adds slog functionality to the HTTP client. // It attempts to obtain a logger with [FromContext]. // If no logger is in the context, it tries to use a fallback logger, // which might be set by [WithFallbackLogger]. // If no logger was found finally, the Transport is // executed without logging. func EnableHTTPClient(c *http.Client, opts ...ClientLoggerOption) { lrt := &logRountTripper{ next: c.Transport, duration: time.Since, reqToAttr: requestToAttr, resToAttr: responseToAttr, } if lrt.next == nil { lrt.next = http.DefaultTransport } for _, opt := range opts { opt(lrt) } c.Transport = lrt } type logRountTripper struct { next http.RoundTripper duration func(time.Time) time.Duration fallback *slog.Logger group string reqToAttr func(*http.Request) slog.Attr resToAttr func(*http.Response) slog.Attr } // RoundTrip implements [http.RoundTripper]. func (l *logRountTripper) RoundTrip(req *http.Request) (*http.Response, error) { logger, ok := l.fromContextOrFallback(req.Context()) if !ok { return l.next.RoundTrip(req) } start := time.Now() resp, err := l.next.RoundTrip(req) logger = logger.WithGroup(l.group).With( l.reqToAttr(req), slog.Duration("duration", l.duration(start)), ) if err != nil { logger.Error("request roundtrip", "error", err) return resp, err } logger.Info("request roundtrip", l.resToAttr(resp)) return resp, nil } func (l *logRountTripper) fromContextOrFallback(ctx context.Context) (*slog.Logger, bool) { if logger, ok := FromContext(ctx); ok { return logger, ok } return l.fallback, l.fallback != nil } golang-github-zitadel-logging-0.6.1/http_client_test.go000066400000000000000000000051511467764427300232540ustar00rootroot00000000000000package logging import ( "fmt" "io" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type errRountripper struct{} func (errRountripper) RoundTrip(*http.Request) (*http.Response, error) { return nil, io.ErrClosedPipe } func Test_EnableHTTPClient(t *testing.T) { tests := []struct { name string transport http.RoundTripper fromCtx bool wantErr error wantLog string }{ { name: "nil transport / default", transport: nil, wantLog: `{ "level":"INFO", "msg":"request roundtrip", "time":"not", "request":{"method":"GET","url":"%s"}, "duration":1000000000, "response":{ "status":"200 OK", "content_length":14 } }`, }, { name: "transport set", transport: http.DefaultTransport, wantLog: `{ "level":"INFO", "msg":"request roundtrip", "time":"not", "request":{"method":"GET","url":"%s"}, "duration":1000000000, "response":{ "status":"200 OK", "content_length":14 } }`, }, { name: "roundtrip error", transport: errRountripper{}, wantErr: io.ErrClosedPipe, wantLog: `{ "level":"ERROR", "msg":"request roundtrip", "time":"not", "request":{"method":"GET","url":"%s"}, "error":"io: read/write on closed pipe", "duration":1000000000 }`, }, { name: "logger from ctx", transport: http.DefaultTransport, fromCtx: true, wantLog: `{ "level":"INFO", "msg":"request roundtrip", "time":"not", "ctx":{ "request":{"method":"GET","url":"%s"}, "duration":1000000000, "response":{ "status":"200 OK", "content_length":14 } } }`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { out, logger := newTestLogger() c := &http.Client{ Transport: tt.transport, } EnableHTTPClient(c, WithFallbackLogger(logger), WithClientDurationFunc(func(t time.Time) time.Duration { return time.Second }), WithClientRequestAttr(requestToAttr), WithClientResponseAttr(responseToAttr), ) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, client") })) defer ts.Close() req, err := http.NewRequest(http.MethodGet, ts.URL, nil) require.NoError(t, err) if tt.fromCtx { req = req.WithContext(ToContext(req.Context(), logger.WithGroup("ctx"))) } _, err = c.Do(req) require.ErrorIs(t, err, tt.wantErr) wantLog := fmt.Sprintf(tt.wantLog, ts.URL) assert.JSONEq(t, wantLog, out.String()) }) } } golang-github-zitadel-logging-0.6.1/logger.go000066400000000000000000000011201467764427300211470ustar00rootroot00000000000000package logging import ( "io" "github.com/sirupsen/logrus" ) type logger logrus.Logger var log *logger = (*logger)(logrus.StandardLogger()) func SetOutput(out io.Writer) { (*logrus.Logger)(log).SetOutput(out) } func SetFormatter(formatter logrus.Formatter) { (*logrus.Logger)(log).SetFormatter(formatter) } func SetLevel(level logrus.Level) { (*logrus.Logger)(log).SetLevel(level) } func SetGlobal() { logrus.SetFormatter(log.Formatter) logrus.SetLevel(log.Level) logrus.SetReportCaller(log.ReportCaller) logrus.SetOutput(log.Out) log = (*logger)(logrus.StandardLogger()) } golang-github-zitadel-logging-0.6.1/logging.go000066400000000000000000000174441467764427300213360ustar00rootroot00000000000000package logging import ( "fmt" "runtime" "time" "github.com/sirupsen/logrus" ) type Entry struct { *logrus.Entry isOnError bool err error } var idKey = "logID" // SetIDKey key of id in logentry func SetIDKey(key string) { idKey = key } // Deprecated: Log creates a new entry with an id func Log(id string) *Entry { entry := (*logrus.Logger)(log).WithField(idKey, id) entry.Logger = (*logrus.Logger)(log) return &Entry{Entry: entry} } // Deprecated: LogWithFields creates a new entry with an id and the given fields func LogWithFields(id string, fields ...interface{}) *Entry { e := Log(id) return e.SetFields(fields...) } // New instantiates a new entry func New() *Entry { return &Entry{Entry: logrus.NewEntry((*logrus.Logger)(log))} } func OnError(err error) *Entry { e := New() return e.OnError(err) } func WithError(err error) *Entry { e := New() return e.WithError(err) } // WithFields creates a new entry without an id and the given fields func WithFields(fields ...interface{}) *Entry { return New().SetFields(fields...) } // OnError sets the error. The log will only be printed if err is not nil func (e *Entry) OnError(err error) *Entry { e.err = err e.isOnError = true return e } // SetFields sets the given fields on the entry. It panics if length of fields is odd func (e *Entry) SetFields(fields ...interface{}) *Entry { logFields := toFields(fields...) return e.WithFields(logFields) } func (e *Entry) WithField(key string, value interface{}) *Entry { e.Entry = e.Entry.WithField(key, value) return e } func (e *Entry) WithFields(fields logrus.Fields) *Entry { e.Entry = e.Entry.WithFields(fields) return e } func (e *Entry) WithError(err error) *Entry { e.Entry = e.Entry.WithError(err) return e } func (e *Entry) WithTime(t time.Time) *Entry { e.Entry = e.Entry.WithTime(t) return e } func toFields(fields ...interface{}) logrus.Fields { if len(fields)%2 != 0 { return logrus.Fields{"oddFields": len(fields)} } logFields := make(logrus.Fields, len(fields)%2) for i := 0; i < len(fields); i = i + 2 { key := fields[i].(string) logFields[key] = fields[i+1] } return logFields } func Debug(args ...interface{}) { e := New() e.log(func() { e.Entry.Debug(args...) }) } func (e *Entry) Debug(args ...interface{}) { e.log(func() { e.Entry.Debug(args...) }) } func Debugln(args ...interface{}) { e := New() e.log(func() { e.Entry.Debugln(args...) }) } func (e *Entry) Debugln(args ...interface{}) { e.log(func() { e.Entry.Debugln(args...) }) } func Debugf(format string, args ...interface{}) { e := New() e.log(func() { e.Entry.Debugf(format, args...) }) } func (e *Entry) Debugf(format string, args ...interface{}) { e.log(func() { e.Entry.Debugf(format, args...) }) } func Info(args ...interface{}) { e := New() e.log(func() { e.Entry.Info(args...) }) } func (e *Entry) Info(args ...interface{}) { e.log(func() { e.Entry.Info(args...) }) } func Infoln(args ...interface{}) { e := New() e.log(func() { e.Entry.Infoln(args...) }) } func (e *Entry) Infoln(args ...interface{}) { e.log(func() { e.Entry.Infoln(args...) }) } func Infof(format string, args ...interface{}) { e := New() e.log(func() { e.Entry.Infof(format, args...) }) } func (e *Entry) Infof(format string, args ...interface{}) { e.log(func() { e.Entry.Infof(format, args...) }) } func Trace(args ...interface{}) { e := New() e.log(func() { e.Entry.Trace(args...) }) } func (e *Entry) Trace(args ...interface{}) { e.log(func() { e.Entry.Trace(args...) }) } func Traceln(args ...interface{}) { e := New() e.log(func() { e.Entry.Traceln(args...) }) } func (e *Entry) Traceln(args ...interface{}) { e.log(func() { e.Entry.Traceln(args...) }) } func Tracef(format string, args ...interface{}) { e := New() e.log(func() { e.Entry.Tracef(format, args...) }) } func (e *Entry) Tracef(format string, args ...interface{}) { e.log(func() { e.Entry.Tracef(format, args...) }) } func Warn(args ...interface{}) { e := New() e.log(func() { e.Entry.Warn(args...) }) } func (e *Entry) Warn(args ...interface{}) { e.log(func() { e.Entry.Warn(args...) }) } func Warnln(args ...interface{}) { e := New() e.log(func() { e.Entry.Warnln(args...) }) } func (e *Entry) Warnln(args ...interface{}) { e.log(func() { e.Entry.Warnln(args...) }) } func Warnf(format string, args ...interface{}) { e := New() e.log(func() { e.Entry.Warnf(format, args...) }) } func (e *Entry) Warnf(format string, args ...interface{}) { e.log(func() { e.Entry.Warnf(format, args...) }) } func Warning(args ...interface{}) { e := New() e.log(func() { e.Entry.Warning(args...) }) } func (e *Entry) Warning(args ...interface{}) { e.log(func() { e.Entry.Warning(args...) }) } func Warningln(args ...interface{}) { e := New() e.log(func() { e.Entry.Warningln(args...) }) } func (e *Entry) Warningln(args ...interface{}) { e.log(func() { e.Entry.Warningln(args...) }) } func Warningf(format string, args ...interface{}) { e := New() e.log(func() { e.Entry.Warningf(format, args...) }) } func (e *Entry) Warningf(format string, args ...interface{}) { e.log(func() { e.Entry.Warningf(format, args...) }) } func Error(args ...interface{}) { e := New() e.log(func() { e.Entry.Error(args...) }) } func (e *Entry) Error(args ...interface{}) { e.log(func() { e.Entry.Error(args...) }) } func Errorln(args ...interface{}) { e := New() e.log(func() { e.Entry.Errorln(args...) }) } func (e *Entry) Errorln(args ...interface{}) { e.log(func() { e.Entry.Errorln(args...) }) } func Errorf(format string, args ...interface{}) { e := New() e.log(func() { e.Entry.Errorf(format, args...) }) } func (e *Entry) Errorf(format string, args ...interface{}) { e.log(func() { e.Entry.Errorf(format, args...) }) } func Fatal(args ...interface{}) { e := New() e.log(func() { e.Entry.Fatal(args...) }) } func (e *Entry) Fatal(args ...interface{}) { e.log(func() { e.Entry.Fatal(args...) }) } func Fatalln(args ...interface{}) { e := New() e.log(func() { e.Entry.Fatalln(args...) }) } func (e *Entry) Fatalln(args ...interface{}) { e.log(func() { e.Entry.Fatalln(args...) }) } func Fatalf(format string, args ...interface{}) { e := New() e.log(func() { e.Entry.Fatalf(format, args...) }) } func (e *Entry) Fatalf(format string, args ...interface{}) { e.log(func() { e.Entry.Fatalf(format, args...) }) } func Panic(args ...interface{}) { e := New() e.log(func() { e.Entry.Panic(args...) }) } func (e *Entry) Panic(args ...interface{}) { e.log(func() { e.Entry.Panic(args...) }) } func Panicln(args ...interface{}) { e := New() e.log(func() { e.Entry.Panicln(args...) }) } func (e *Entry) Panicln(args ...interface{}) { e.log(func() { e.Entry.Panic(args...) }) } func Panicf(format string, args ...interface{}) { e := New() e.log(func() { e.Entry.Panicf(format, args...) }) } func (e *Entry) Panicf(format string, args ...interface{}) { e.log(func() { e.Entry.Panicf(format, args...) }) } func (e *Entry) Log(level logrus.Level, args ...interface{}) { e.log(func() { e.Entry.Log(level, args...) }) } func Logf(level logrus.Level, format string, args ...interface{}) { e := New() e.log(func() { e.Entry.Logf(level, format, args...) }) } func (e *Entry) Logf(level logrus.Level, format string, args ...interface{}) { e.log(func() { e.Entry.Logf(level, format, args...) }) } func Logln(level logrus.Level, args ...interface{}) { e := New() e.log(func() { e.Entry.Logln(level, args...) }) } func (e *Entry) Logln(level logrus.Level, args ...interface{}) { e.log(func() { e.Entry.Logln(level, args...) }) } func (e *Entry) log(log func()) { e = e.checkOnError() if e == nil { return } addCaller(e) log() } func (e *Entry) checkOnError() *Entry { if !e.isOnError { return e } if e.err != nil { return e.WithError(e.err) } return nil } func addCaller(e *Entry) { _, file, no, ok := runtime.Caller(3) if ok { e.WithField("caller", fmt.Sprintf("%s:%d", file, no)) } } golang-github-zitadel-logging-0.6.1/logging_test.go000066400000000000000000000174001467764427300223650ustar00rootroot00000000000000package logging import ( "errors" "fmt" "strings" "testing" ) var errTest = fmt.Errorf("im an error") func TestWithLogID(t *testing.T) { tests := []struct { name string entry *Entry expectedFields map[string]func(interface{}) bool }{ { "without error", Log("UTILS-B7l7"), map[string]func(interface{}) bool{ "logID": func(got interface{}) bool { logID, ok := got.(string) if !ok { return false } return logID == "UTILS-B7l7" }, "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, }, }, { "with error", Log("UTILS-Ld9V").WithError(errTest), map[string]func(interface{}) bool{ "logID": func(got interface{}) bool { logID, ok := got.(string) if !ok { return false } return logID == "UTILS-Ld9V" }, "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, "error": func(got interface{}) bool { err, ok := got.(error) if !ok { return false } return errors.Is(err, errTest) }, }, }, { "on error", Log("UTILS-Ld9V").OnError(errTest), map[string]func(interface{}) bool{ "logID": func(got interface{}) bool { logID, ok := got.(string) if !ok { return false } return logID == "UTILS-Ld9V" }, "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, "error": func(got interface{}) bool { err, ok := got.(error) if !ok { return false } return errors.Is(err, errTest) }, }, }, { "on error without", Log("UTILS-Ld9V").OnError(nil), map[string]func(interface{}) bool{ "logID": func(got interface{}) bool { logID, ok := got.(string) if !ok { return false } return logID == "UTILS-Ld9V" }, }, }, { "with fields", LogWithFields("LOGGI-5kk6z", "field1", 134, "field2", "asdlkfj"), map[string]func(interface{}) bool{ "logID": func(got interface{}) bool { logID, ok := got.(string) if !ok { return false } return logID == "LOGGI-5kk6z" }, "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, "field1": func(got interface{}) bool { i, ok := got.(int) if !ok { return false } return i == 134 }, "field2": func(got interface{}) bool { i, ok := got.(string) if !ok { return false } return i == "asdlkfj" }, }, }, { "with field", LogWithFields("LOGGI-5kk6z").WithField("field1", 134), map[string]func(interface{}) bool{ "logID": func(got interface{}) bool { logID, ok := got.(string) if !ok { return false } return logID == "LOGGI-5kk6z" }, "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, "field1": func(got interface{}) bool { i, ok := got.(int) if !ok { return false } return i == 134 }, }, }, { "fields odd", LogWithFields("LOGGI-xWzy4", "kevin"), map[string]func(interface{}) bool{ "logID": func(got interface{}) bool { logID, ok := got.(string) if !ok { return false } return logID == "LOGGI-xWzy4" }, "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, "oddFields": func(got interface{}) bool { i, ok := got.(int) if !ok { return false } return i == 1 }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { test.entry.Debug() if len(test.entry.Data) != len(test.expectedFields) { t.Errorf("enexpected amount of fields got: %d, want %d", len(test.entry.Data), len(test.expectedFields)) } for key, expectedValue := range test.expectedFields { value, ok := test.entry.Data[key] if !ok { t.Errorf("\"%s\" was not expected", key) } if !expectedValue(value) { t.Errorf("wrong value for \"%s\": got %T.%v", key, value, value) } } }) } } func TestWithoutLogID(t *testing.T) { tests := []struct { name string entry *Entry expectedFields map[string]func(interface{}) bool }{ { "without error", New(), map[string]func(interface{}) bool{ "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, }, }, { "with error", New().WithError(errTest), map[string]func(interface{}) bool{ "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, "error": func(got interface{}) bool { err, ok := got.(error) if !ok { return false } return errors.Is(err, errTest) }, }, }, { "on error", OnError(errTest), map[string]func(interface{}) bool{ "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, "error": func(got interface{}) bool { err, ok := got.(error) if !ok { return false } return errors.Is(err, errTest) }, }, }, { "on error without", OnError(nil), map[string]func(interface{}) bool{}, }, { "with fields", WithFields("field1", 134, "field2", "asdlkfj"), map[string]func(interface{}) bool{ "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, "field1": func(got interface{}) bool { i, ok := got.(int) if !ok { return false } return i == 134 }, "field2": func(got interface{}) bool { i, ok := got.(string) if !ok { return false } return i == "asdlkfj" }, }, }, { "with field", WithFields().WithField("field1", 134), map[string]func(interface{}) bool{ "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, "field1": func(got interface{}) bool { i, ok := got.(int) if !ok { return false } return i == 134 }, }, }, { "fields odd", WithFields("kevin"), map[string]func(interface{}) bool{ "caller": func(got interface{}) bool { s, ok := got.(string) if !ok { return false } return strings.Contains(s, "logging/logging_test.go:") }, "oddFields": func(got interface{}) bool { i, ok := got.(int) if !ok { return false } return i == 1 }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { test.entry.Debug() if len(test.entry.Data) != len(test.expectedFields) { t.Errorf("enexpected amount of fields got: %d, want %d", len(test.entry.Data), len(test.expectedFields)) } for key, expectedValue := range test.expectedFields { value, ok := test.entry.Data[key] if !ok { t.Errorf("\"%s\" was not expected", key) } if !expectedValue(value) { t.Errorf("wrong value for \"%s\": got %T.%v", key, value, value) } } }) } } golang-github-zitadel-logging-0.6.1/middleware.go000066400000000000000000000072711467764427300220220ustar00rootroot00000000000000package logging import ( "net/http" "time" "log/slog" ) type MiddlewareOption func(*middleware) // WitLogger sets the passed logger with request attributes // into the Request's context. func WithLogger(logger *slog.Logger) MiddlewareOption { return func(m *middleware) { m.logger = logger } } // WithGroup groups the log attributes // produced by the middleware. func WithGroup(name string) MiddlewareOption { return func(m *middleware) { m.group = name } } // WithIDFunc enables the creating of request IDs // in the middleware, which are then attached to // the logger. func WithIDFunc(nextID func() slog.Attr) MiddlewareOption { return func(m *middleware) { m.nextID = nextID } } // WithDurationFunc allows overriding the request duration for testing. func WithDurationFunc(df func(time.Time) time.Duration) MiddlewareOption { return func(m *middleware) { m.duration = df } } // WithRequestAttr allows customizing the information used // from a request as request attributes. func WithRequestAttr(requestToAttr func(*http.Request) slog.Attr) MiddlewareOption { return func(m *middleware) { m.reqAttr = requestToAttr } } // WithLoggedWriter allows customizing the writer from // which post-request attributes are taken. func WithLoggedWriter(wrap func(w http.ResponseWriter) LoggedWriter) MiddlewareOption { return func(m *middleware) { m.wrapWriter = wrap } } // Middleware enables request logging and sets a logger // to the request context. // Use [FromContext] to obtain the logger anywhere in the request liftime. // // The default logger is [slog.Default], with the request's URL and Method // as preset attributes. // When the request terminates, a INFO line with the Status Code and // amount written to the client is printed. // This behaviors can be modified with options. func Middleware(options ...MiddlewareOption) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { mw := &middleware{ logger: slog.Default(), duration: time.Since, next: next, reqAttr: requestToAttr, wrapWriter: newLoggedWriter, } for _, opt := range options { opt(mw) } return mw } } type middleware struct { logger *slog.Logger group string nextID func() slog.Attr next http.Handler duration func(time.Time) time.Duration reqAttr func(*http.Request) slog.Attr wrapWriter func(http.ResponseWriter) LoggedWriter } func (m *middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { start := time.Now() logger := m.logger.With(slog.Group(m.group, m.reqAttr(r))) if m.nextID != nil { logger = logger.With(slog.Group(m.group, m.nextID())) } r = r.WithContext(ToContext(r.Context(), logger)) lw := m.wrapWriter(w) m.next.ServeHTTP(lw, r) logger = logger.With(slog.Group(m.group, slog.Duration("duration", m.duration(start)), lw.Attr(), )) if err := lw.Err(); err != nil { logger.WarnContext(r.Context(), "write response", "error", err) return } logger.InfoContext(r.Context(), "request served") } type loggedWriter struct { http.ResponseWriter statusCode int written int err error } func newLoggedWriter(w http.ResponseWriter) LoggedWriter { return &loggedWriter{ ResponseWriter: w, } } func (w *loggedWriter) WriteHeader(statusCode int) { w.statusCode = statusCode w.ResponseWriter.WriteHeader(statusCode) } func (w *loggedWriter) Write(b []byte) (int, error) { if w.statusCode == 0 { w.WriteHeader(http.StatusOK) } n, err := w.ResponseWriter.Write(b) w.written += n w.err = err return n, err } func (lw *loggedWriter) Attr() slog.Attr { return slog.Group("response", "status", lw.statusCode, "written", lw.written, ) } func (lw *loggedWriter) Err() error { return lw.err } golang-github-zitadel-logging-0.6.1/middleware_test.go000066400000000000000000000043051467764427300230540ustar00rootroot00000000000000package logging import ( "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "log/slog" "github.com/stretchr/testify/assert" ) func newTestLogger() (out *strings.Builder, logger *slog.Logger) { out = new(strings.Builder) handler := slog.NewJSONHandler(out, &slog.HandlerOptions{ Level: slog.LevelDebug, }).WithAttrs([]slog.Attr{slog.String("time", "not")}) return out, slog.New(handler) } type testWriter struct { *httptest.ResponseRecorder err error } func (w *testWriter) Write(b []byte) (int, error) { if w.err != nil { return 0, w.err } return w.ResponseRecorder.Write(b) } func newTestWriter(err error) *testWriter { return &testWriter{ ResponseRecorder: httptest.NewRecorder(), err: err, } } func TestMiddleware(t *testing.T) { tests := []struct { name string err error want string }{ { name: "ok", want: `{ "level":"INFO", "time": "not", "msg":"request served", "id":"id1", "duration":1000000000, "request":{ "method":"GET", "url":"https://example.com/path/" }, "response":{ "status":200, "written":13 } }`, }, { name: "error", err: io.ErrClosedPipe, want: `{ "level":"WARN", "time": "not", "msg":"write response", "error": "io: read/write on closed pipe", "id":"id1", "duration":1000000000, "request":{ "method":"GET", "url":"https://example.com/path/" }, "response":{ "status":200, "written":0 } }`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { logOut, logger := newTestLogger() mw := Middleware( WithLogger(logger), WithIDFunc(func() slog.Attr { return slog.String("id", "id1") }), WithDurationFunc(func(time.Time) time.Duration { return time.Second }), WithRequestAttr(requestToAttr), WithLoggedWriter(newLoggedWriter), ) next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello, World!") }) w := newTestWriter(tt.err) r := httptest.NewRequest("GET", "https://example.com/path/", nil) mw(next).ServeHTTP(w, r) got := logOut.String() assert.JSONEq(t, tt.want, got) }) } }