├── .github ├── dependabot.yml └── workflows │ ├── apidiff.yaml │ ├── assign.yaml │ ├── lint.yaml │ ├── scorecard.yml │ └── tests.yaml ├── .golangci.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── _tools └── apidiff.sh ├── benchmark ├── README.md ├── benchmark_slog_test.go └── benchmark_test.go ├── context.go ├── context_noslog.go ├── context_slog.go ├── context_slog_test.go ├── context_test.go ├── discard.go ├── discard_test.go ├── example_marshaler_secret_test.go ├── example_marshaler_test.go ├── example_slogr_test.go ├── example_test.go ├── examples ├── slog │ └── main.go ├── tab_logger.go └── usage_example.go ├── funcr ├── example │ ├── main.go │ ├── main_noslog.go │ └── main_slog.go ├── example_formatter_test.go ├── example_test.go ├── funcr.go ├── funcr_test.go ├── slogsink.go └── slogsink_test.go ├── go.mod ├── internal └── testhelp │ ├── slog.go │ └── slog_test.go ├── logr.go ├── logr_test.go ├── sloghandler.go ├── slogr.go ├── slogr └── slogr.go ├── slogr_test.go ├── slogsink.go ├── testimpls_slog_test.go ├── testimpls_test.go ├── testing ├── test.go └── test_test.go └── testr ├── testr.go ├── testr_fuzz_test.go └── testr_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/apidiff.yaml: -------------------------------------------------------------------------------- 1 | name: Run apidiff 2 | 3 | on: [ pull_request ] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | apidiff: 10 | runs-on: ubuntu-latest 11 | if: github.base_ref 12 | steps: 13 | - name: Install Go 14 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 15 | with: 16 | go-version: 1.24.x 17 | - name: Add GOBIN to PATH 18 | run: echo "PATH=$(go env GOPATH)/bin:$PATH" >>$GITHUB_ENV 19 | - name: Install dependencies 20 | run: go install golang.org/x/exp/cmd/apidiff@latest 21 | - name: Checkout old code 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | with: 24 | ref: ${{ github.base_ref }} 25 | path: "old" 26 | - name: Checkout new code 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | with: 29 | path: "new" 30 | - name: APIDiff 31 | run: ./_tools/apidiff.sh -d ../old 32 | working-directory: "new" 33 | -------------------------------------------------------------------------------- /.github/workflows/assign.yaml: -------------------------------------------------------------------------------- 1 | name: Assign 2 | 3 | on: 4 | issues: 5 | types: [opened, reopened] 6 | pull_request_target: 7 | types: [opened, reopened] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | assign: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | issues: write 17 | pull-requests: write 18 | steps: 19 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 20 | with: 21 | script: | 22 | github.rest.issues.addAssignees({ 23 | issue_number: context.issue.number, 24 | owner: context.repo.owner, 25 | repo: context.repo.repo, 26 | assignees: ['thockin', 'pohly'] 27 | }) 28 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Run lint 2 | 3 | on: [ push, pull_request ] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | - name: Update Go 15 | uses: actions/setup-go@v5.5.0 16 | with: 17 | go-version: '>=1.21.0' 18 | cache: false 19 | - name: Lint 20 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 21 | with: 22 | # version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 23 | version: latest 24 | 25 | # Optional: show only new issues if it's a pull request. The default value is `false`. 26 | # only-new-issues: true 27 | 28 | # Read args from .golangci.yaml 29 | # args: 30 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '28 21 * * 1' 14 | push: 15 | branches: [ "master" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | 30 | steps: 31 | - name: "Checkout code" 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | with: 34 | persist-credentials: false 35 | 36 | - name: "Run analysis" 37 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 38 | with: 39 | results_file: results.sarif 40 | results_format: sarif 41 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 42 | # you want to enable the Branch-Protection check on a *public* repository, or 43 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 44 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 45 | 46 | # - Publish results to OpenSSF REST API for easy access by consumers 47 | # - Allows the repository to include the Scorecard badge. 48 | # - See https://github.com/ossf/scorecard-action#publishing-results. 49 | publish_results: true 50 | 51 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 52 | # format to the repository Actions tab. 53 | - name: "Upload artifact" 54 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 55 | with: 56 | name: SARIF file 57 | path: results.sarif 58 | retention-days: 5 59 | 60 | # Upload the results to GitHub's code scanning dashboard. 61 | - name: "Upload to code-scanning" 62 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 63 | with: 64 | sarif_file: results.sarif 65 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [ push, pull_request ] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | matrix: 12 | version: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24' ] 13 | platform: [ ubuntu-latest, macos-latest, windows-latest ] 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - name: Install Go 17 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 18 | with: 19 | go-version: ${{ matrix.version }} 20 | - name: Checkout code 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - name: Build 23 | run: go build -v ./... 24 | - name: Test 25 | run: go test -v -race ./... 26 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | run: 4 | timeout: 1m 5 | tests: true 6 | 7 | linters: 8 | default: none 9 | enable: # please keep this alphabetized 10 | - asasalint 11 | - asciicheck 12 | - copyloopvar 13 | - dupl 14 | - errcheck 15 | - forcetypeassert 16 | - goconst 17 | - gocritic 18 | - govet 19 | - ineffassign 20 | - misspell 21 | - musttag 22 | - revive 23 | - staticcheck 24 | - unused 25 | 26 | issues: 27 | max-issues-per-linter: 0 28 | max-same-issues: 10 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v1.0.0-rc1 4 | 5 | This is the first logged release. Major changes (including breaking changes) 6 | have occurred since earlier tags. 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Logr is open to pull-requests, provided they fit within the intended scope of 4 | the project. Specifically, this library aims to be VERY small and minimalist, 5 | with no external dependencies. 6 | 7 | ## Compatibility 8 | 9 | This project intends to follow [semantic versioning](http://semver.org) and 10 | is very strict about compatibility. Any proposed changes MUST follow those 11 | rules. 12 | 13 | ## Performance 14 | 15 | As a logging library, logr must be as light-weight as possible. Any proposed 16 | code change must include results of running the [benchmark](./benchmark) 17 | before and after the change. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A minimal logging API for Go 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/go-logr/logr.svg)](https://pkg.go.dev/github.com/go-logr/logr) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-logr/logr)](https://goreportcard.com/report/github.com/go-logr/logr) 5 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/go-logr/logr/badge)](https://securityscorecards.dev/viewer/?platform=github.com&org=go-logr&repo=logr) 6 | 7 | logr offers an(other) opinion on how Go programs and libraries can do logging 8 | without becoming coupled to a particular logging implementation. This is not 9 | an implementation of logging - it is an API. In fact it is two APIs with two 10 | different sets of users. 11 | 12 | The `Logger` type is intended for application and library authors. It provides 13 | a relatively small API which can be used everywhere you want to emit logs. It 14 | defers the actual act of writing logs (to files, to stdout, or whatever) to the 15 | `LogSink` interface. 16 | 17 | The `LogSink` interface is intended for logging library implementers. It is a 18 | pure interface which can be implemented by logging frameworks to provide the actual logging 19 | functionality. 20 | 21 | This decoupling allows application and library developers to write code in 22 | terms of `logr.Logger` (which has very low dependency fan-out) while the 23 | implementation of logging is managed "up stack" (e.g. in or near `main()`.) 24 | Application developers can then switch out implementations as necessary. 25 | 26 | Many people assert that libraries should not be logging, and as such efforts 27 | like this are pointless. Those people are welcome to convince the authors of 28 | the tens-of-thousands of libraries that *DO* write logs that they are all 29 | wrong. In the meantime, logr takes a more practical approach. 30 | 31 | ## Typical usage 32 | 33 | Somewhere, early in an application's life, it will make a decision about which 34 | logging library (implementation) it actually wants to use. Something like: 35 | 36 | ``` 37 | func main() { 38 | // ... other setup code ... 39 | 40 | // Create the "root" logger. We have chosen the "logimpl" implementation, 41 | // which takes some initial parameters and returns a logr.Logger. 42 | logger := logimpl.New(param1, param2) 43 | 44 | // ... other setup code ... 45 | ``` 46 | 47 | Most apps will call into other libraries, create structures to govern the flow, 48 | etc. The `logr.Logger` object can be passed to these other libraries, stored 49 | in structs, or even used as a package-global variable, if needed. For example: 50 | 51 | ``` 52 | app := createTheAppObject(logger) 53 | app.Run() 54 | ``` 55 | 56 | Outside of this early setup, no other packages need to know about the choice of 57 | implementation. They write logs in terms of the `logr.Logger` that they 58 | received: 59 | 60 | ``` 61 | type appObject struct { 62 | // ... other fields ... 63 | logger logr.Logger 64 | // ... other fields ... 65 | } 66 | 67 | func (app *appObject) Run() { 68 | app.logger.Info("starting up", "timestamp", time.Now()) 69 | 70 | // ... app code ... 71 | ``` 72 | 73 | ## Background 74 | 75 | If the Go standard library had defined an interface for logging, this project 76 | probably would not be needed. Alas, here we are. 77 | 78 | When the Go developers started developing such an interface with 79 | [slog](https://github.com/golang/go/issues/56345), they adopted some of the 80 | logr design but also left out some parts and changed others: 81 | 82 | | Feature | logr | slog | 83 | |---------|------|------| 84 | | High-level API | `Logger` (passed by value) | `Logger` (passed by [pointer](https://github.com/golang/go/issues/59126)) | 85 | | Low-level API | `LogSink` | `Handler` | 86 | | Stack unwinding | done by `LogSink` | done by `Logger` | 87 | | Skipping helper functions | `WithCallDepth`, `WithCallStackHelper` | [not supported by Logger](https://github.com/golang/go/issues/59145) | 88 | | Generating a value for logging on demand | `Marshaler` | `LogValuer` | 89 | | Log levels | >= 0, higher meaning "less important" | positive and negative, with 0 for "info" and higher meaning "more important" | 90 | | Error log entries | always logged, don't have a verbosity level | normal log entries with level >= `LevelError` | 91 | | Passing logger via context | `NewContext`, `FromContext` | no API | 92 | | Adding a name to a logger | `WithName` | no API | 93 | | Modify verbosity of log entries in a call chain | `V` | no API | 94 | | Grouping of key/value pairs | not supported | `WithGroup`, `GroupValue` | 95 | | Pass context for extracting additional values | no API | API variants like `InfoCtx` | 96 | 97 | The high-level slog API is explicitly meant to be one of many different APIs 98 | that can be layered on top of a shared `slog.Handler`. logr is one such 99 | alternative API, with [interoperability](#slog-interoperability) provided by 100 | some conversion functions. 101 | 102 | ### Inspiration 103 | 104 | Before you consider this package, please read [this blog post by the 105 | inimitable Dave Cheney][warning-makes-no-sense]. We really appreciate what 106 | he has to say, and it largely aligns with our own experiences. 107 | 108 | ### Differences from Dave's ideas 109 | 110 | The main differences are: 111 | 112 | 1. Dave basically proposes doing away with the notion of a logging API in favor 113 | of `fmt.Printf()`. We disagree, especially when you consider things like output 114 | locations, timestamps, file and line decorations, and structured logging. This 115 | package restricts the logging API to just 2 types of logs: info and error. 116 | 117 | Info logs are things you want to tell the user which are not errors. Error 118 | logs are, well, errors. If your code receives an `error` from a subordinate 119 | function call and is logging that `error` *and not returning it*, use error 120 | logs. 121 | 122 | 2. Verbosity-levels on info logs. This gives developers a chance to indicate 123 | arbitrary grades of importance for info logs, without assigning names with 124 | semantic meaning such as "warning", "trace", and "debug." Superficially this 125 | may feel very similar, but the primary difference is the lack of semantics. 126 | Because verbosity is a numerical value, it's safe to assume that an app running 127 | with higher verbosity means more (and less important) logs will be generated. 128 | 129 | ## Implementations (non-exhaustive) 130 | 131 | There are implementations for the following logging libraries: 132 | 133 | - **a function** (can bridge to non-structured libraries): [funcr](https://github.com/go-logr/logr/tree/master/funcr) 134 | - **a testing.T** (for use in Go tests, with JSON-like output): [testr](https://github.com/go-logr/logr/tree/master/testr) 135 | - **github.com/google/glog**: [glogr](https://github.com/go-logr/glogr) 136 | - **k8s.io/klog** (for Kubernetes): [klogr](https://git.k8s.io/klog/klogr) 137 | - **a testing.T** (with klog-like text output): [ktesting](https://git.k8s.io/klog/ktesting) 138 | - **go.uber.org/zap**: [zapr](https://github.com/go-logr/zapr) 139 | - **log** (the Go standard library logger): [stdr](https://github.com/go-logr/stdr) 140 | - **github.com/sirupsen/logrus**: [logrusr](https://github.com/bombsimon/logrusr) 141 | - **github.com/wojas/genericr**: [genericr](https://github.com/wojas/genericr) (makes it easy to implement your own backend) 142 | - **logfmt** (Heroku style [logging](https://www.brandur.org/logfmt)): [logfmtr](https://github.com/iand/logfmtr) 143 | - **github.com/rs/zerolog**: [zerologr](https://github.com/go-logr/zerologr) 144 | - **github.com/go-kit/log**: [gokitlogr](https://github.com/tonglil/gokitlogr) (also compatible with github.com/go-kit/kit/log since v0.12.0) 145 | - **bytes.Buffer** (writing to a buffer): [bufrlogr](https://github.com/tonglil/buflogr) (useful for ensuring values were logged, like during testing) 146 | 147 | ## slog interoperability 148 | 149 | Interoperability goes both ways, using the `logr.Logger` API with a `slog.Handler` 150 | and using the `slog.Logger` API with a `logr.LogSink`. `FromSlogHandler` and 151 | `ToSlogHandler` convert between a `logr.Logger` and a `slog.Handler`. 152 | As usual, `slog.New` can be used to wrap such a `slog.Handler` in the high-level 153 | slog API. 154 | 155 | ### Using a `logr.LogSink` as backend for slog 156 | 157 | Ideally, a logr sink implementation should support both logr and slog by 158 | implementing both the normal logr interface(s) and `SlogSink`. Because 159 | of a conflict in the parameters of the common `Enabled` method, it is [not 160 | possible to implement both slog.Handler and logr.Sink in the same 161 | type](https://github.com/golang/go/issues/59110). 162 | 163 | If both are supported, log calls can go from the high-level APIs to the backend 164 | without the need to convert parameters. `FromSlogHandler` and `ToSlogHandler` can 165 | convert back and forth without adding additional wrappers, with one exception: 166 | when `Logger.V` was used to adjust the verbosity for a `slog.Handler`, then 167 | `ToSlogHandler` has to use a wrapper which adjusts the verbosity for future 168 | log calls. 169 | 170 | Such an implementation should also support values that implement specific 171 | interfaces from both packages for logging (`logr.Marshaler`, `slog.LogValuer`, 172 | `slog.GroupValue`). logr does not convert those. 173 | 174 | Not supporting slog has several drawbacks: 175 | - Recording source code locations works correctly if the handler gets called 176 | through `slog.Logger`, but may be wrong in other cases. That's because a 177 | `logr.Sink` does its own stack unwinding instead of using the program counter 178 | provided by the high-level API. 179 | - slog levels <= 0 can be mapped to logr levels by negating the level without a 180 | loss of information. But all slog levels > 0 (e.g. `slog.LevelWarning` as 181 | used by `slog.Logger.Warn`) must be mapped to 0 before calling the sink 182 | because logr does not support "more important than info" levels. 183 | - The slog group concept is supported by prefixing each key in a key/value 184 | pair with the group names, separated by a dot. For structured output like 185 | JSON it would be better to group the key/value pairs inside an object. 186 | - Special slog values and interfaces don't work as expected. 187 | - The overhead is likely to be higher. 188 | 189 | These drawbacks are severe enough that applications using a mixture of slog and 190 | logr should switch to a different backend. 191 | 192 | ### Using a `slog.Handler` as backend for logr 193 | 194 | Using a plain `slog.Handler` without support for logr works better than the 195 | other direction: 196 | - All logr verbosity levels can be mapped 1:1 to their corresponding slog level 197 | by negating them. 198 | - Stack unwinding is done by the `SlogSink` and the resulting program 199 | counter is passed to the `slog.Handler`. 200 | - Names added via `Logger.WithName` are gathered and recorded in an additional 201 | attribute with `logger` as key and the names separated by slash as value. 202 | - `Logger.Error` is turned into a log record with `slog.LevelError` as level 203 | and an additional attribute with `err` as key, if an error was provided. 204 | 205 | The main drawback is that `logr.Marshaler` will not be supported. Types should 206 | ideally support both `logr.Marshaler` and `slog.Valuer`. If compatibility 207 | with logr implementations without slog support is not important, then 208 | `slog.Valuer` is sufficient. 209 | 210 | ### Context support for slog 211 | 212 | Storing a logger in a `context.Context` is not supported by 213 | slog. `NewContextWithSlogLogger` and `FromContextAsSlogLogger` can be 214 | used to fill this gap. They store and retrieve a `slog.Logger` pointer 215 | under the same context key that is also used by `NewContext` and 216 | `FromContext` for `logr.Logger` value. 217 | 218 | When `NewContextWithSlogLogger` is followed by `FromContext`, the latter will 219 | automatically convert the `slog.Logger` to a 220 | `logr.Logger`. `FromContextAsSlogLogger` does the same for the other direction. 221 | 222 | With this approach, binaries which use either slog or logr are as efficient as 223 | possible with no unnecessary allocations. This is also why the API stores a 224 | `slog.Logger` pointer: when storing a `slog.Handler`, creating a `slog.Logger` 225 | on retrieval would need to allocate one. 226 | 227 | The downside is that switching back and forth needs more allocations. Because 228 | logr is the API that is already in use by different packages, in particular 229 | Kubernetes, the recommendation is to use the `logr.Logger` API in code which 230 | uses contextual logging. 231 | 232 | An alternative to adding values to a logger and storing that logger in the 233 | context is to store the values in the context and to configure a logging 234 | backend to extract those values when emitting log entries. This only works when 235 | log calls are passed the context, which is not supported by the logr API. 236 | 237 | With the slog API, it is possible, but not 238 | required. https://github.com/veqryn/slog-context is a package for slog which 239 | provides additional support code for this approach. It also contains wrappers 240 | for the context functions in logr, so developers who prefer to not use the logr 241 | APIs directly can use those instead and the resulting code will still be 242 | interoperable with logr. 243 | 244 | ## FAQ 245 | 246 | ### Conceptual 247 | 248 | #### Why structured logging? 249 | 250 | - **Structured logs are more easily queryable**: Since you've got 251 | key-value pairs, it's much easier to query your structured logs for 252 | particular values by filtering on the contents of a particular key -- 253 | think searching request logs for error codes, Kubernetes reconcilers for 254 | the name and namespace of the reconciled object, etc. 255 | 256 | - **Structured logging makes it easier to have cross-referenceable logs**: 257 | Similarly to searchability, if you maintain conventions around your 258 | keys, it becomes easy to gather all log lines related to a particular 259 | concept. 260 | 261 | - **Structured logs allow better dimensions of filtering**: if you have 262 | structure to your logs, you've got more precise control over how much 263 | information is logged -- you might choose in a particular configuration 264 | to log certain keys but not others, only log lines where a certain key 265 | matches a certain value, etc., instead of just having v-levels and names 266 | to key off of. 267 | 268 | - **Structured logs better represent structured data**: sometimes, the 269 | data that you want to log is inherently structured (think tuple-link 270 | objects.) Structured logs allow you to preserve that structure when 271 | outputting. 272 | 273 | #### Why V-levels? 274 | 275 | **V-levels give operators an easy way to control the chattiness of log 276 | operations**. V-levels provide a way for a given package to distinguish 277 | the relative importance or verbosity of a given log message. Then, if 278 | a particular logger or package is logging too many messages, the user 279 | of the package can simply change the v-levels for that library. 280 | 281 | #### Why not named levels, like Info/Warning/Error? 282 | 283 | Read [Dave Cheney's post][warning-makes-no-sense]. Then read [Differences 284 | from Dave's ideas](#differences-from-daves-ideas). 285 | 286 | #### Why not allow format strings, too? 287 | 288 | **Format strings negate many of the benefits of structured logs**: 289 | 290 | - They're not easily searchable without resorting to fuzzy searching, 291 | regular expressions, etc. 292 | 293 | - They don't store structured data well, since contents are flattened into 294 | a string. 295 | 296 | - They're not cross-referenceable. 297 | 298 | - They don't compress easily, since the message is not constant. 299 | 300 | (Unless you turn positional parameters into key-value pairs with numerical 301 | keys, at which point you've gotten key-value logging with meaningless 302 | keys.) 303 | 304 | ### Practical 305 | 306 | #### Why key-value pairs, and not a map? 307 | 308 | Key-value pairs are *much* easier to optimize, especially around 309 | allocations. Zap (a structured logger that inspired logr's interface) has 310 | [performance measurements](https://github.com/uber-go/zap#performance) 311 | that show this quite nicely. 312 | 313 | While the interface ends up being a little less obvious, you get 314 | potentially better performance, plus avoid making users type 315 | `map[string]string{}` every time they want to log. 316 | 317 | #### What if my V-levels differ between libraries? 318 | 319 | That's fine. Control your V-levels on a per-logger basis, and use the 320 | `WithName` method to pass different loggers to different libraries. 321 | 322 | Generally, you should take care to ensure that you have relatively 323 | consistent V-levels within a given logger, however, as this makes deciding 324 | on what verbosity of logs to request easier. 325 | 326 | #### But I really want to use a format string! 327 | 328 | That's not actually a question. Assuming your question is "how do 329 | I convert my mental model of logging with format strings to logging with 330 | constant messages": 331 | 332 | 1. Figure out what the error actually is, as you'd write in a TL;DR style, 333 | and use that as a message. 334 | 335 | 2. For every place you'd write a format specifier, look to the word before 336 | it, and add that as a key value pair. 337 | 338 | For instance, consider the following examples (all taken from spots in the 339 | Kubernetes codebase): 340 | 341 | - `klog.V(4).Infof("Client is returning errors: code %v, error %v", 342 | responseCode, err)` becomes `logger.Error(err, "client returned an 343 | error", "code", responseCode)` 344 | 345 | - `klog.V(4).Infof("Got a Retry-After %ds response for attempt %d to %v", 346 | seconds, retries, url)` becomes `logger.V(4).Info("got a retry-after 347 | response when requesting url", "attempt", retries, "after 348 | seconds", seconds, "url", url)` 349 | 350 | If you *really* must use a format string, use it in a key's value, and 351 | call `fmt.Sprintf` yourself. For instance: `log.Printf("unable to 352 | reflect over type %T")` becomes `logger.Info("unable to reflect over 353 | type", "type", fmt.Sprintf("%T"))`. In general though, the cases where 354 | this is necessary should be few and far between. 355 | 356 | #### How do I choose my V-levels? 357 | 358 | This is basically the only hard constraint: increase V-levels to denote 359 | more verbose or more debug-y logs. 360 | 361 | Otherwise, you can start out with `0` as "you always want to see this", 362 | `1` as "common logging that you might *possibly* want to turn off", and 363 | `10` as "I would like to performance-test your log collection stack." 364 | 365 | Then gradually choose levels in between as you need them, working your way 366 | down from 10 (for debug and trace style logs) and up from 1 (for chattier 367 | info-type logs). For reference, slog pre-defines -4 for debug logs 368 | (corresponds to 4 in logr), which matches what is 369 | [recommended for Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#what-method-to-use). 370 | 371 | #### How do I choose my keys? 372 | 373 | Keys are fairly flexible, and can hold more or less any string 374 | value. For best compatibility with implementations and consistency 375 | with existing code in other projects, there are a few conventions you 376 | should consider. 377 | 378 | - Make your keys human-readable. 379 | - Constant keys are generally a good idea. 380 | - Be consistent across your codebase. 381 | - Keys should naturally match parts of the message string. 382 | - Use lower case for simple keys and 383 | [lowerCamelCase](https://en.wiktionary.org/wiki/lowerCamelCase) for 384 | more complex ones. Kubernetes is one example of a project that has 385 | [adopted that 386 | convention](https://github.com/kubernetes/community/blob/HEAD/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments). 387 | 388 | While key names are mostly unrestricted (and spaces are acceptable), 389 | it's generally a good idea to stick to printable ascii characters, or at 390 | least match the general character set of your log lines. 391 | 392 | #### Why should keys be constant values? 393 | 394 | The point of structured logging is to make later log processing easier. Your 395 | keys are, effectively, the schema of each log message. If you use different 396 | keys across instances of the same log line, you will make your structured logs 397 | much harder to use. `Sprintf()` is for values, not for keys! 398 | 399 | #### Why is this not a pure interface? 400 | 401 | The Logger type is implemented as a struct in order to allow the Go compiler to 402 | optimize things like high-V `Info` logs that are not triggered. Not all of 403 | these implementations are implemented yet, but this structure was suggested as 404 | a way to ensure they *can* be implemented. All of the real work is behind the 405 | `LogSink` interface. 406 | 407 | [warning-makes-no-sense]: http://dave.cheney.net/2015/11/05/lets-talk-about-logging 408 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you have discovered a security vulnerability in this project, please report it 4 | privately. **Do not disclose it as a public issue.** This gives us time to work with you 5 | to fix the issue before public exposure, reducing the chance that the exploit will be 6 | used before a patch is released. 7 | 8 | You may submit the report in the following ways: 9 | 10 | - send an email to go-logr-security@googlegroups.com 11 | - send us a [private vulnerability report](https://github.com/go-logr/logr/security/advisories/new) 12 | 13 | Please provide the following information in your report: 14 | 15 | - A description of the vulnerability and its impact 16 | - How to reproduce the issue 17 | 18 | We ask that you give us 90 days to work on a fix before public exposure. 19 | -------------------------------------------------------------------------------- /_tools/apidiff.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # Copyright 2021 The logr Authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | set -o errexit 19 | set -o nounset 20 | set -o pipefail 21 | 22 | function usage { 23 | local script="$(basename $0)" 24 | 25 | echo >&2 "Usage: ${script} [-r | -d ] 26 | 27 | This script should be run at the root of a module. 28 | 29 | -r 30 | Compare the exported API of the local working copy with the 31 | exported API of the local repo at the specified branch or tag. 32 | 33 | -d 34 | Compare the exported API of the local working copy with the 35 | exported API of the specified directory, which should point 36 | to the root of a different version of the same module. 37 | 38 | Examples: 39 | ${script} -r master 40 | ${script} -r v1.10.0 41 | ${script} -r release-1.10 42 | ${script} -d /path/to/historical/version 43 | " 44 | exit 1 45 | } 46 | 47 | ref="" 48 | dir="" 49 | while getopts r:d: o 50 | do case "$o" in 51 | r) ref="$OPTARG";; 52 | d) dir="$OPTARG";; 53 | [?]) usage;; 54 | esac 55 | done 56 | 57 | # If REF and DIR are empty, print usage and error 58 | if [[ -z "${ref}" && -z "${dir}" ]]; then 59 | usage; 60 | fi 61 | # If REF and DIR are both set, print usage and error 62 | if [[ -n "${ref}" && -n "${dir}" ]]; then 63 | usage; 64 | fi 65 | 66 | if ! which apidiff > /dev/null; then 67 | echo "Installing golang.org/x/exp/cmd/apidiff" 68 | pushd "${TMPDIR:-/tmp}" > /dev/null 69 | GO111MODULE=off go get golang.org/x/exp/cmd/apidiff 70 | popd > /dev/null 71 | fi 72 | 73 | output=$(mktemp -d -t "apidiff.output.XXXX") 74 | cleanup_output () { rm -fr "${output}"; } 75 | trap cleanup_output EXIT 76 | 77 | # If ref is set, clone . to temp dir at $ref, and set $dir to the temp dir 78 | clone="" 79 | base="${dir}" 80 | if [[ -n "${ref}" ]]; then 81 | base="${ref}" 82 | clone=$(mktemp -d -t "apidiff.clone.XXXX") 83 | cleanup_clone_and_output () { rm -fr "${clone}"; cleanup_output; } 84 | trap cleanup_clone_and_output EXIT 85 | git clone . -q --no-tags "${clone}" 86 | git -C "${clone}" co "${ref}" 87 | dir="${clone}" 88 | fi 89 | 90 | pushd "${dir}" >/dev/null 91 | echo "Inspecting API of ${base}" 92 | go list ./... > packages.txt 93 | for pkg in $(cat packages.txt); do 94 | mkdir -p "${output}/${pkg}" 95 | apidiff -w "${output}/${pkg}/apidiff.output" "${pkg}" 96 | done 97 | popd >/dev/null 98 | 99 | retval=0 100 | 101 | echo "Comparing with ${base}" 102 | for pkg in $(go list ./...); do 103 | # New packages are ok 104 | if [ ! -f "${output}/${pkg}/apidiff.output" ]; then 105 | continue 106 | fi 107 | 108 | # Check for incompatible changes to previous packages 109 | incompatible=$(apidiff -incompatible "${output}/${pkg}/apidiff.output" "${pkg}") 110 | if [[ -n "${incompatible}" ]]; then 111 | echo >&2 "FAIL: ${pkg} contains incompatible changes: 112 | ${incompatible} 113 | " 114 | retval=1 115 | fi 116 | done 117 | 118 | # Check for removed packages 119 | removed=$(comm -23 "${dir}/packages.txt" <(go list ./...)) 120 | if [[ -n "${removed}" ]]; then 121 | echo >&2 "FAIL: removed packages: 122 | ${removed} 123 | " 124 | retval=1 125 | fi 126 | 127 | exit $retval 128 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarking logr 2 | 3 | Any major changes to the logr library must be benchmarked before and after the 4 | change. 5 | 6 | ## Running the benchmark 7 | 8 | ``` 9 | $ go test -bench='.' -test.benchmem ./benchmark/ 10 | ``` 11 | 12 | ## Fixing the benchmark 13 | 14 | If you think this benchmark can be improved, you are probably correct! PRs are 15 | very welcome. 16 | -------------------------------------------------------------------------------- /benchmark/benchmark_slog_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2021 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package logr 21 | 22 | import ( 23 | "log/slog" 24 | "os" 25 | "testing" 26 | 27 | "github.com/go-logr/logr" 28 | ) 29 | 30 | // 31 | // slogSink wrapper of discard 32 | // 33 | 34 | func BenchmarkSlogSinkLogInfoOneArg(b *testing.B) { 35 | var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck 36 | doInfoOneArg(b, log) 37 | } 38 | 39 | func BenchmarkSlogSinkLogInfoSeveralArgs(b *testing.B) { 40 | var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck 41 | doInfoSeveralArgs(b, log) 42 | } 43 | 44 | func BenchmarkSlogSinkLogInfoWithValues(b *testing.B) { 45 | var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck 46 | doInfoWithValues(b, log) 47 | } 48 | 49 | func BenchmarkSlogSinkLogV0Info(b *testing.B) { 50 | var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck 51 | doV0Info(b, log) 52 | } 53 | 54 | func BenchmarkSlogSinkLogV9Info(b *testing.B) { 55 | var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck 56 | doV9Info(b, log) 57 | } 58 | 59 | func BenchmarkSlogSinkLogError(b *testing.B) { 60 | var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck 61 | doError(b, log) 62 | } 63 | 64 | func BenchmarkSlogSinkWithValues(b *testing.B) { 65 | var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck 66 | doWithValues(b, log) 67 | } 68 | 69 | func BenchmarkSlogSinkWithName(b *testing.B) { 70 | var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck 71 | doWithName(b, log) 72 | } 73 | 74 | // 75 | // slogSink wrapper of slog's JSONHandler, for comparison 76 | // 77 | 78 | func makeSlogJSONLogger() logr.Logger { 79 | devnull, _ := os.Open("/dev/null") 80 | handler := slog.NewJSONHandler(devnull, nil) 81 | return logr.FromSlogHandler(handler) 82 | } 83 | 84 | func BenchmarkSlogJSONLogInfoOneArg(b *testing.B) { 85 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 86 | doInfoOneArg(b, log) 87 | } 88 | 89 | func BenchmarkSlogJSONLogInfoSeveralArgs(b *testing.B) { 90 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 91 | doInfoSeveralArgs(b, log) 92 | } 93 | 94 | func BenchmarkSlogJSONLogInfoWithValues(b *testing.B) { 95 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 96 | doInfoWithValues(b, log) 97 | } 98 | 99 | func BenchmarkSlogJSONLogV0Info(b *testing.B) { 100 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 101 | doV0Info(b, log) 102 | } 103 | 104 | func BenchmarkSlogJSONLogV9Info(b *testing.B) { 105 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 106 | doV9Info(b, log) 107 | } 108 | 109 | func BenchmarkSlogJSONLogError(b *testing.B) { 110 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 111 | doError(b, log) 112 | } 113 | 114 | func BenchmarkSlogJSONLogWithValues(b *testing.B) { 115 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 116 | doWithValues(b, log) 117 | } 118 | 119 | func BenchmarkSlogJSONWithName(b *testing.B) { 120 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 121 | doWithName(b, log) 122 | } 123 | 124 | func BenchmarkSlogJSONWithCallDepth(b *testing.B) { 125 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 126 | doWithCallDepth(b, log) 127 | } 128 | 129 | func BenchmarkSlogJSONLogInfoStringerValue(b *testing.B) { 130 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 131 | doStringerValue(b, log) 132 | } 133 | 134 | func BenchmarkSlogJSONLogInfoErrorValue(b *testing.B) { 135 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 136 | doErrorValue(b, log) 137 | } 138 | 139 | func BenchmarkSlogJSONLogInfoMarshalerValue(b *testing.B) { 140 | var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck 141 | doMarshalerValue(b, log) 142 | } 143 | -------------------------------------------------------------------------------- /benchmark/benchmark_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logr 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/go-logr/logr" 24 | "github.com/go-logr/logr/funcr" 25 | ) 26 | 27 | //go:noinline 28 | func doInfoOneArg(b *testing.B, log logr.Logger) { 29 | for i := 0; i < b.N; i++ { 30 | log.Info("this is", "a", "string") 31 | } 32 | } 33 | 34 | //go:noinline 35 | func doInfoSeveralArgs(b *testing.B, log logr.Logger) { 36 | for i := 0; i < b.N; i++ { 37 | log.Info("multi", 38 | "bool", true, "string", "str", "int", 42, 39 | "float", 3.14, "struct", struct{ X, Y int }{93, 76}) 40 | } 41 | } 42 | 43 | //go:noinline 44 | func doInfoWithValues(b *testing.B, log logr.Logger) { 45 | log = log.WithValues("k1", "str", "k2", 222, "k3", true, "k4", 1.0) 46 | for i := 0; i < b.N; i++ { 47 | log.Info("multi", 48 | "bool", true, "string", "str", "int", 42, 49 | "float", 3.14, "struct", struct{ X, Y int }{93, 76}) 50 | } 51 | } 52 | 53 | //go:noinline 54 | func doV0Info(b *testing.B, log logr.Logger) { 55 | for i := 0; i < b.N; i++ { 56 | log.V(0).Info("multi", 57 | "bool", true, "string", "str", "int", 42, 58 | "float", 3.14, "struct", struct{ X, Y int }{93, 76}) 59 | } 60 | } 61 | 62 | //go:noinline 63 | func doV9Info(b *testing.B, log logr.Logger) { 64 | for i := 0; i < b.N; i++ { 65 | log.V(9).Info("multi", 66 | "bool", true, "string", "str", "int", 42, 67 | "float", 3.14, "struct", struct{ X, Y int }{93, 76}) 68 | } 69 | } 70 | 71 | //go:noinline 72 | func doError(b *testing.B, log logr.Logger) { 73 | err := fmt.Errorf("error message") 74 | for i := 0; i < b.N; i++ { 75 | log.Error(err, "multi", 76 | "bool", true, "string", "str", "int", 42, 77 | "float", 3.14, "struct", struct{ X, Y int }{93, 76}) 78 | } 79 | } 80 | 81 | //go:noinline 82 | func doWithValues(b *testing.B, log logr.Logger) { 83 | for i := 0; i < b.N; i++ { 84 | l := log.WithValues("k1", "v1", "k2", "v2") 85 | _ = l 86 | } 87 | } 88 | 89 | //go:noinline 90 | func doWithName(b *testing.B, log logr.Logger) { 91 | for i := 0; i < b.N; i++ { 92 | l := log.WithName("name") 93 | _ = l 94 | } 95 | } 96 | 97 | //go:noinline 98 | func doWithCallDepth(b *testing.B, log logr.Logger) { 99 | for i := 0; i < b.N; i++ { 100 | l := log.WithCallDepth(1) 101 | _ = l 102 | } 103 | } 104 | 105 | type Tstringer struct{ s string } 106 | 107 | func (t Tstringer) String() string { 108 | return t.s 109 | } 110 | 111 | //go:noinline 112 | func doStringerValue(b *testing.B, log logr.Logger) { 113 | for i := 0; i < b.N; i++ { 114 | log.Info("this is", "a", Tstringer{"stringer"}) 115 | } 116 | } 117 | 118 | type Terror struct{ s string } 119 | 120 | func (t Terror) Error() string { 121 | return t.s 122 | } 123 | 124 | //go:noinline 125 | func doErrorValue(b *testing.B, log logr.Logger) { 126 | for i := 0; i < b.N; i++ { 127 | log.Info("this is", "an", Terror{"error"}) 128 | } 129 | } 130 | 131 | type Tmarshaler struct{ s string } 132 | 133 | func (t Tmarshaler) MarshalLog() any { 134 | return t.s 135 | } 136 | 137 | //go:noinline 138 | func doMarshalerValue(b *testing.B, log logr.Logger) { 139 | for i := 0; i < b.N; i++ { 140 | log.Info("this is", "a", Tmarshaler{"marshaler"}) 141 | } 142 | } 143 | 144 | // 145 | // discard 146 | // 147 | 148 | func BenchmarkDiscardLogInfoOneArg(b *testing.B) { 149 | var log logr.Logger = logr.Discard() //nolint:staticcheck 150 | doInfoOneArg(b, log) 151 | } 152 | 153 | func BenchmarkDiscardLogInfoSeveralArgs(b *testing.B) { 154 | var log logr.Logger = logr.Discard() //nolint:staticcheck 155 | doInfoSeveralArgs(b, log) 156 | } 157 | 158 | func BenchmarkDiscardLogInfoWithValues(b *testing.B) { 159 | var log logr.Logger = logr.Discard() //nolint:staticcheck 160 | doInfoWithValues(b, log) 161 | } 162 | 163 | func BenchmarkDiscardLogV0Info(b *testing.B) { 164 | var log logr.Logger = logr.Discard() //nolint:staticcheck 165 | doV0Info(b, log) 166 | } 167 | 168 | func BenchmarkDiscardLogV9Info(b *testing.B) { 169 | var log logr.Logger = logr.Discard() //nolint:staticcheck 170 | doV9Info(b, log) 171 | } 172 | 173 | func BenchmarkDiscardLogError(b *testing.B) { 174 | var log logr.Logger = logr.Discard() //nolint:staticcheck 175 | doError(b, log) 176 | } 177 | 178 | func BenchmarkDiscardWithValues(b *testing.B) { 179 | var log logr.Logger = logr.Discard() //nolint:staticcheck 180 | doWithValues(b, log) 181 | } 182 | 183 | func BenchmarkDiscardWithName(b *testing.B) { 184 | var log logr.Logger = logr.Discard() //nolint:staticcheck 185 | doWithName(b, log) 186 | } 187 | 188 | // 189 | // funcr 190 | // 191 | 192 | func noopKV(_, _ string) {} 193 | func noopJSON(_ string) {} 194 | 195 | func BenchmarkFuncrLogInfoOneArg(b *testing.B) { 196 | var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck 197 | doInfoOneArg(b, log) 198 | } 199 | 200 | func BenchmarkFuncrJSONLogInfoOneArg(b *testing.B) { 201 | var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck 202 | doInfoOneArg(b, log) 203 | } 204 | 205 | func BenchmarkFuncrLogInfoSeveralArgs(b *testing.B) { 206 | var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck 207 | doInfoSeveralArgs(b, log) 208 | } 209 | 210 | func BenchmarkFuncrJSONLogInfoSeveralArgs(b *testing.B) { 211 | var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck 212 | doInfoSeveralArgs(b, log) 213 | } 214 | 215 | func BenchmarkFuncrLogInfoWithValues(b *testing.B) { 216 | var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck 217 | doInfoWithValues(b, log) 218 | } 219 | 220 | func BenchmarkFuncrJSONLogInfoWithValues(b *testing.B) { 221 | var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck 222 | doInfoWithValues(b, log) 223 | } 224 | 225 | func BenchmarkFuncrLogV0Info(b *testing.B) { 226 | var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck 227 | doV0Info(b, log) 228 | } 229 | 230 | func BenchmarkFuncrJSONLogV0Info(b *testing.B) { 231 | var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck 232 | doV0Info(b, log) 233 | } 234 | 235 | func BenchmarkFuncrLogV9Info(b *testing.B) { 236 | var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck 237 | doV9Info(b, log) 238 | } 239 | 240 | func BenchmarkFuncrJSONLogV9Info(b *testing.B) { 241 | var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck 242 | doV9Info(b, log) 243 | } 244 | 245 | func BenchmarkFuncrLogError(b *testing.B) { 246 | var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck 247 | doError(b, log) 248 | } 249 | 250 | func BenchmarkFuncrJSONLogError(b *testing.B) { 251 | var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck 252 | doError(b, log) 253 | } 254 | 255 | func BenchmarkFuncrWithValues(b *testing.B) { 256 | var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck 257 | doWithValues(b, log) 258 | } 259 | 260 | func BenchmarkFuncrWithName(b *testing.B) { 261 | var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck 262 | doWithName(b, log) 263 | } 264 | 265 | func BenchmarkFuncrWithCallDepth(b *testing.B) { 266 | var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck 267 | doWithCallDepth(b, log) 268 | } 269 | 270 | func BenchmarkFuncrJSONLogInfoStringerValue(b *testing.B) { 271 | var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck 272 | doStringerValue(b, log) 273 | } 274 | 275 | func BenchmarkFuncrJSONLogInfoErrorValue(b *testing.B) { 276 | var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck 277 | doErrorValue(b, log) 278 | } 279 | 280 | func BenchmarkFuncrJSONLogInfoMarshalerValue(b *testing.B) { 281 | var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck 282 | doMarshalerValue(b, log) 283 | } 284 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logr 18 | 19 | // contextKey is how we find Loggers in a context.Context. With Go < 1.21, 20 | // the value is always a Logger value. With Go >= 1.21, the value can be a 21 | // Logger value or a slog.Logger pointer. 22 | type contextKey struct{} 23 | 24 | // notFoundError exists to carry an IsNotFound method. 25 | type notFoundError struct{} 26 | 27 | func (notFoundError) Error() string { 28 | return "no logr.Logger was present" 29 | } 30 | 31 | func (notFoundError) IsNotFound() bool { 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /context_noslog.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.21 2 | // +build !go1.21 3 | 4 | /* 5 | Copyright 2019 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package logr 21 | 22 | import ( 23 | "context" 24 | ) 25 | 26 | // FromContext returns a Logger from ctx or an error if no Logger is found. 27 | func FromContext(ctx context.Context) (Logger, error) { 28 | if v, ok := ctx.Value(contextKey{}).(Logger); ok { 29 | return v, nil 30 | } 31 | 32 | return Logger{}, notFoundError{} 33 | } 34 | 35 | // FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this 36 | // returns a Logger that discards all log messages. 37 | func FromContextOrDiscard(ctx context.Context) Logger { 38 | if v, ok := ctx.Value(contextKey{}).(Logger); ok { 39 | return v 40 | } 41 | 42 | return Discard() 43 | } 44 | 45 | // NewContext returns a new Context, derived from ctx, which carries the 46 | // provided Logger. 47 | func NewContext(ctx context.Context, logger Logger) context.Context { 48 | return context.WithValue(ctx, contextKey{}, logger) 49 | } 50 | -------------------------------------------------------------------------------- /context_slog.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2019 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package logr 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "log/slog" 26 | ) 27 | 28 | // FromContext returns a Logger from ctx or an error if no Logger is found. 29 | func FromContext(ctx context.Context) (Logger, error) { 30 | v := ctx.Value(contextKey{}) 31 | if v == nil { 32 | return Logger{}, notFoundError{} 33 | } 34 | 35 | switch v := v.(type) { 36 | case Logger: 37 | return v, nil 38 | case *slog.Logger: 39 | return FromSlogHandler(v.Handler()), nil 40 | default: 41 | // Not reached. 42 | panic(fmt.Sprintf("unexpected value type for logr context key: %T", v)) 43 | } 44 | } 45 | 46 | // FromContextAsSlogLogger returns a slog.Logger from ctx or nil if no such Logger is found. 47 | func FromContextAsSlogLogger(ctx context.Context) *slog.Logger { 48 | v := ctx.Value(contextKey{}) 49 | if v == nil { 50 | return nil 51 | } 52 | 53 | switch v := v.(type) { 54 | case Logger: 55 | return slog.New(ToSlogHandler(v)) 56 | case *slog.Logger: 57 | return v 58 | default: 59 | // Not reached. 60 | panic(fmt.Sprintf("unexpected value type for logr context key: %T", v)) 61 | } 62 | } 63 | 64 | // FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this 65 | // returns a Logger that discards all log messages. 66 | func FromContextOrDiscard(ctx context.Context) Logger { 67 | if logger, err := FromContext(ctx); err == nil { 68 | return logger 69 | } 70 | return Discard() 71 | } 72 | 73 | // NewContext returns a new Context, derived from ctx, which carries the 74 | // provided Logger. 75 | func NewContext(ctx context.Context, logger Logger) context.Context { 76 | return context.WithValue(ctx, contextKey{}, logger) 77 | } 78 | 79 | // NewContextWithSlogLogger returns a new Context, derived from ctx, which carries the 80 | // provided slog.Logger. 81 | func NewContextWithSlogLogger(ctx context.Context, logger *slog.Logger) context.Context { 82 | return context.WithValue(ctx, contextKey{}, logger) 83 | } 84 | -------------------------------------------------------------------------------- /context_slog_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2021 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package logr 21 | 22 | import ( 23 | "context" 24 | "log/slog" 25 | "os" 26 | "testing" 27 | ) 28 | 29 | func TestContextWithSlog(t *testing.T) { 30 | ctx := context.Background() 31 | 32 | if out := FromContextAsSlogLogger(ctx); out != nil { 33 | t.Errorf("expected no logger, got %#v", out) 34 | } 35 | 36 | // Write as slog... 37 | slogger := slog.New(slog.NewJSONHandler(os.Stderr, nil)) 38 | sctx := NewContextWithSlogLogger(ctx, slogger) 39 | 40 | // ...read as logr 41 | if out, err := FromContext(sctx); err != nil { 42 | t.Errorf("unexpected error: %v", err) 43 | } else if _, ok := out.sink.(*slogSink); !ok { 44 | t.Errorf("expected output to be type *logr.slogSink, got %T", out.sink) 45 | } 46 | 47 | // ...read as slog 48 | if out := FromContextAsSlogLogger(sctx); out == nil { 49 | t.Errorf("expected a *slog.JSONHandler, got nil") 50 | } else if _, ok := out.Handler().(*slog.JSONHandler); !ok { 51 | t.Errorf("expected output to be type *slog.JSONHandler, got %T", out.Handler()) 52 | } 53 | 54 | // Write as logr... 55 | logger := Discard() 56 | lctx := NewContext(ctx, logger) 57 | 58 | // ...read as slog 59 | if out := FromContextAsSlogLogger(lctx); out == nil { 60 | t.Errorf("expected a *log.slogHandler, got nil") 61 | } else if _, ok := out.Handler().(*slogHandler); !ok { 62 | t.Errorf("expected output to be type *logr.slogHandler, got %T", out.Handler()) 63 | } 64 | 65 | // ...read as logr is covered in the non-slog test 66 | } 67 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logr 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | ) 23 | 24 | func TestContext(t *testing.T) { 25 | ctx := context.Background() 26 | 27 | if out, err := FromContext(ctx); err == nil { 28 | t.Errorf("expected error, got %#v", out) 29 | } else if _, ok := err.(notFoundError); !ok { 30 | t.Errorf("expected a notFoundError, got %#v", err) 31 | } 32 | 33 | out := FromContextOrDiscard(ctx) 34 | if out.sink != nil { 35 | t.Errorf("expected a nil sink, got %#v", out) 36 | } 37 | 38 | sink := &testLogSink{} 39 | logger := New(sink) 40 | lctx := NewContext(ctx, logger) 41 | if out, err := FromContext(lctx); err != nil { 42 | t.Errorf("unexpected error: %v", err) 43 | } else if p, _ := out.sink.(*testLogSink); p != sink { 44 | t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) 45 | } 46 | out = FromContextOrDiscard(lctx) 47 | if p, _ := out.sink.(*testLogSink); p != sink { 48 | t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /discard.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logr 18 | 19 | // Discard returns a Logger that discards all messages logged to it. It can be 20 | // used whenever the caller is not interested in the logs. Logger instances 21 | // produced by this function always compare as equal. 22 | func Discard() Logger { 23 | return New(nil) 24 | } 25 | -------------------------------------------------------------------------------- /discard_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logr 18 | 19 | import ( 20 | "errors" 21 | "reflect" 22 | "testing" 23 | ) 24 | 25 | func TestDiscard(t *testing.T) { 26 | l := Discard() 27 | if l.GetSink() != nil { 28 | t.Error("did not return the expected underlying type") 29 | } 30 | // Verify that none of the methods panic, there is not more we can test. 31 | l.WithName("discard").WithValues("z", 5).Info("Hello world") 32 | l.Info("Hello world", "x", 1, "y", 2) 33 | l.V(1).Error(errors.New("foo"), "a", 123) 34 | if l.Enabled() { 35 | t.Error("discard loggers must always be disabled") 36 | } 37 | } 38 | 39 | func TestComparable(t *testing.T) { 40 | a := Discard() 41 | if !reflect.TypeOf(a).Comparable() { 42 | t.Fatal("discard loggers must be comparable") 43 | } 44 | 45 | b := Discard() 46 | if a != b { 47 | t.Fatal("any two discard Loggers must be equal") 48 | } 49 | 50 | c := Discard().V(2) 51 | if b != c { 52 | t.Fatal("any two discard Loggers must be equal") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example_marshaler_secret_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logr_test 18 | 19 | import ( 20 | "github.com/go-logr/logr" 21 | ) 22 | 23 | // ComplexObjectRef contains more fields than it wants to get logged. 24 | type ComplexObjectRef struct { 25 | Name string 26 | Namespace string 27 | Secret string 28 | } 29 | 30 | func (ref ComplexObjectRef) MarshalLog() any { 31 | return struct { 32 | Name, Namespace string 33 | }{ 34 | Name: ref.Name, 35 | Namespace: ref.Namespace, 36 | } 37 | } 38 | 39 | var _ logr.Marshaler = ComplexObjectRef{} 40 | 41 | func ExampleMarshaler_secret() { 42 | l := NewStdoutLogger() 43 | secret := ComplexObjectRef{Namespace: "kube-system", Name: "some-secret", Secret: "do-not-log-me"} 44 | l.Info("simplified", "secret", secret) 45 | // Output: 46 | // "level"=0 "msg"="simplified" "secret"={"Name"="some-secret" "Namespace"="kube-system"} 47 | } 48 | -------------------------------------------------------------------------------- /example_marshaler_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logr_test 18 | 19 | import ( 20 | "github.com/go-logr/logr" 21 | ) 22 | 23 | // ObjectRef references a Kubernetes object 24 | type ObjectRef struct { 25 | Name string `json:"name"` 26 | Namespace string `json:"namespace,omitempty"` 27 | } 28 | 29 | func (ref ObjectRef) String() string { 30 | if ref.Namespace != "" { 31 | return ref.Namespace + "/" + ref.Name 32 | } 33 | return ref.Name 34 | } 35 | 36 | func (ref ObjectRef) MarshalLog() any { 37 | // We implement fmt.Stringer for non-structured logging, but we want the 38 | // raw struct when using structured logs. Some logr implementations call 39 | // String if it is present, so we want to convert this struct to something 40 | // that doesn't have that method. 41 | type forLog ObjectRef // methods do not survive type definitions 42 | return forLog(ref) 43 | } 44 | 45 | var _ logr.Marshaler = ObjectRef{} 46 | 47 | func ExampleMarshaler() { 48 | l := NewStdoutLogger() 49 | pod := ObjectRef{Namespace: "kube-system", Name: "some-pod"} 50 | l.Info("as string", "pod", pod.String()) 51 | l.Info("as struct", "pod", pod) 52 | // Output: 53 | // "level"=0 "msg"="as string" "pod"="kube-system/some-pod" 54 | // "level"=0 "msg"="as struct" "pod"={"name"="some-pod" "namespace"="kube-system"} 55 | } 56 | -------------------------------------------------------------------------------- /example_slogr_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package logr_test 21 | 22 | import ( 23 | "errors" 24 | "fmt" 25 | "log/slog" 26 | "os" 27 | 28 | "github.com/go-logr/logr" 29 | "github.com/go-logr/logr/funcr" 30 | ) 31 | 32 | var debugWithoutTime = &slog.HandlerOptions{ 33 | ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { 34 | if a.Key == "time" { 35 | return slog.Attr{} 36 | } 37 | return a 38 | }, 39 | Level: slog.LevelDebug, 40 | } 41 | 42 | func ExampleFromSlogHandler() { 43 | logrLogger := logr.FromSlogHandler(slog.NewTextHandler(os.Stdout, debugWithoutTime)) 44 | 45 | logrLogger.Info("hello world") 46 | logrLogger.Error(errors.New("fake error"), "ignore me") 47 | logrLogger.WithValues("x", 1, "y", 2).WithValues("str", "abc").WithName("foo").WithName("bar").V(4).Info("with values, verbosity and name") 48 | 49 | // Output: 50 | // level=INFO msg="hello world" 51 | // level=ERROR msg="ignore me" err="fake error" 52 | // level=DEBUG msg="with values, verbosity and name" x=1 y=2 str=abc logger=foo/bar 53 | } 54 | 55 | func ExampleToSlogHandler() { 56 | funcrLogger := funcr.New(func(prefix, args string) { 57 | if prefix != "" { 58 | fmt.Println(prefix, args) 59 | } else { 60 | fmt.Println(args) 61 | } 62 | }, funcr.Options{ 63 | Verbosity: 10, 64 | }) 65 | 66 | slogLogger := slog.New(logr.ToSlogHandler(funcrLogger)) 67 | slogLogger.Info("hello world") 68 | slogLogger.Error("ignore me", "err", errors.New("fake error")) 69 | slogLogger.With("x", 1, "y", 2).WithGroup("group").With("str", "abc").Warn("with values and group") 70 | 71 | slogLogger = slog.New(logr.ToSlogHandler(funcrLogger.V(int(-slog.LevelDebug)))) 72 | slogLogger.Info("info message reduced to debug level") 73 | 74 | // Output: 75 | // "level"=0 "msg"="hello world" 76 | // "msg"="ignore me" "error"=null "err"="fake error" 77 | // "level"=0 "msg"="with values and group" "x"=1 "y"=2 "group"={"str"="abc"} 78 | // "level"=4 "msg"="info message reduced to debug level" 79 | } 80 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logr_test 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/go-logr/logr" 23 | "github.com/go-logr/logr/funcr" 24 | ) 25 | 26 | // NewStdoutLogger returns a logr.Logger that prints to stdout. 27 | func NewStdoutLogger() logr.Logger { 28 | return funcr.New(func(prefix, args string) { 29 | if prefix != "" { 30 | fmt.Printf("%s: %s\n", prefix, args) 31 | } else { 32 | fmt.Println(args) 33 | } 34 | }, funcr.Options{}) 35 | } 36 | 37 | func Example() { 38 | l := NewStdoutLogger() 39 | l.Info("default info log", "stringVal", "value", "intVal", 12345) 40 | l.V(0).Info("V(0) info log", "stringVal", "value", "intVal", 12345) 41 | l.Error(fmt.Errorf("an error"), "error log", "stringVal", "value", "intVal", 12345) 42 | // Output: 43 | // "level"=0 "msg"="default info log" "stringVal"="value" "intVal"=12345 44 | // "level"=0 "msg"="V(0) info log" "stringVal"="value" "intVal"=12345 45 | // "msg"="error log" "error"="an error" "stringVal"="value" "intVal"=12345 46 | } 47 | 48 | func ExampleLogger_Info() { 49 | l := NewStdoutLogger() 50 | l.Info("this is a V(0)-equivalent info log", "stringVal", "value", "intVal", 12345) 51 | // Output: 52 | // "level"=0 "msg"="this is a V(0)-equivalent info log" "stringVal"="value" "intVal"=12345 53 | } 54 | 55 | func ExampleLogger_Error() { 56 | l := NewStdoutLogger() 57 | l.Error(fmt.Errorf("the error"), "this is an error log", "stringVal", "value", "intVal", 12345) 58 | l.Error(nil, "this is an error log with nil error", "stringVal", "value", "intVal", 12345) 59 | // Output: 60 | // "msg"="this is an error log" "error"="the error" "stringVal"="value" "intVal"=12345 61 | // "msg"="this is an error log with nil error" "error"=null "stringVal"="value" "intVal"=12345 62 | } 63 | 64 | func ExampleLogger_WithName() { 65 | l := NewStdoutLogger() 66 | l = l.WithName("name1") 67 | l.Info("this is an info log", "stringVal", "value", "intVal", 12345) 68 | l = l.WithName("name2") 69 | l.Info("this is an info log", "stringVal", "value", "intVal", 12345) 70 | // Output: 71 | // name1: "level"=0 "msg"="this is an info log" "stringVal"="value" "intVal"=12345 72 | // name1/name2: "level"=0 "msg"="this is an info log" "stringVal"="value" "intVal"=12345 73 | } 74 | 75 | func ExampleLogger_WithValues() { 76 | l := NewStdoutLogger() 77 | l = l.WithValues("stringVal", "value", "intVal", 12345) 78 | l = l.WithValues("boolVal", true) 79 | l.Info("this is an info log", "floatVal", 3.1415) 80 | // Output: 81 | // "level"=0 "msg"="this is an info log" "stringVal"="value" "intVal"=12345 "boolVal"=true "floatVal"=3.1415 82 | } 83 | 84 | func ExampleLogger_V() { 85 | l := NewStdoutLogger() 86 | l.V(0).Info("V(0) info log") 87 | l.V(1).Info("V(1) info log") 88 | l.V(2).Info("V(2) info log") 89 | // Output: 90 | // "level"=0 "msg"="V(0) info log" 91 | } 92 | 93 | func ExampleLogger_Enabled() { 94 | l := NewStdoutLogger() 95 | if loggerV := l.V(5); loggerV.Enabled() { 96 | // Do something expensive. 97 | loggerV.Info("this is an expensive log message") 98 | } 99 | // Output: 100 | } 101 | -------------------------------------------------------------------------------- /examples/slog/main.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Package main is an example of using slogr. 21 | package main 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | "log/slog" 27 | "os" 28 | 29 | "github.com/go-logr/logr" 30 | "github.com/go-logr/logr/funcr" 31 | ) 32 | 33 | type e struct { 34 | str string 35 | } 36 | 37 | func (e e) Error() string { 38 | return e.str 39 | } 40 | 41 | func logrHelper(log logr.Logger, msg string) { 42 | logrHelper2(log, msg) 43 | } 44 | 45 | func logrHelper2(log logr.Logger, msg string) { 46 | log.WithCallDepth(2).Info(msg) 47 | } 48 | 49 | func slogHelper(log *slog.Logger, msg string) { 50 | slogHelper2(log, msg) 51 | } 52 | 53 | func slogHelper2(log *slog.Logger, msg string) { 54 | // slog.Logger has no API for skipping helper functions, so this gets logged as call location. 55 | log.Info(msg) 56 | } 57 | 58 | func main() { 59 | opts := slog.HandlerOptions{ 60 | AddSource: true, 61 | Level: slog.Level(-1), 62 | } 63 | handler := slog.NewJSONHandler(os.Stderr, &opts) 64 | logrLogger := logr.FromSlogHandler(handler) 65 | logrExample(logrLogger) 66 | 67 | logrLogger = funcr.NewJSON( 68 | func(obj string) { fmt.Println(obj) }, 69 | funcr.Options{ 70 | LogCaller: funcr.All, 71 | LogTimestamp: true, 72 | Verbosity: 1, 73 | }) 74 | slogLogger := slog.New(logr.ToSlogHandler(logrLogger)) 75 | slogExample(slogLogger) 76 | } 77 | 78 | func logrExample(log logr.Logger) { 79 | log = log.WithName("my") 80 | log = log.WithName("logger") 81 | log = log.WithName("name") 82 | log = log.WithValues("saved", "value") 83 | log.Info("1) hello", "val1", 1, "val2", map[string]int{"k": 1}) 84 | log.V(1).Info("2) you should see this") 85 | log.V(1).V(1).Info("you should NOT see this") 86 | log.Error(nil, "3) uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) 87 | log.Error(e{"an error occurred"}, "4) goodbye", "code", -1) 88 | logrHelper(log, "5) thru a helper") 89 | } 90 | 91 | func slogExample(log *slog.Logger) { 92 | // There's no guarantee that this logs the right source code location. 93 | // It works for Go 1.21.0 by compensating in logr.ToSlogHandler 94 | // for the additional callers, but those might change. 95 | log = log.With("saved", "value") 96 | log.Info("1) hello", "val1", 1, "val2", map[string]int{"k": 1}) 97 | log.Log(context.TODO(), slog.Level(-1), "2) you should see this") 98 | log.Log(context.TODO(), slog.Level(-2), "you should NOT see this") 99 | log.Error("3) uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) 100 | log.Error("4) goodbye", "code", -1, "err", e{"an error occurred"}) 101 | slogHelper(log, "5) thru a helper") 102 | } 103 | -------------------------------------------------------------------------------- /examples/tab_logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package main implements a simple example of a logr.LogSink that logs to 18 | // stderr in a tabular format. It is not intended to be a production logger. 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | "os" 24 | "text/tabwriter" 25 | 26 | "github.com/go-logr/logr" 27 | ) 28 | 29 | // tabLogSink is a sample logr.LogSink that logs to stderr. 30 | // It's terribly inefficient, and is only a basic example. 31 | type tabLogSink struct { 32 | name string 33 | keyValues map[string]any 34 | writer *tabwriter.Writer 35 | } 36 | 37 | var _ logr.LogSink = &tabLogSink{} 38 | 39 | // Note that Init usually takes a pointer so it can modify the receiver to save 40 | // runtime info. 41 | func (*tabLogSink) Init(_ logr.RuntimeInfo) { 42 | } 43 | 44 | func (tabLogSink) Enabled(_ int) bool { 45 | return true 46 | } 47 | 48 | func (l tabLogSink) Info(_ int, msg string, kvs ...any) { 49 | _, _ = fmt.Fprintf(l.writer, "%s\t%s\t", l.name, msg) 50 | for k, v := range l.keyValues { 51 | _, _ = fmt.Fprintf(l.writer, "%s: %+v ", k, v) 52 | } 53 | for i := 0; i < len(kvs); i += 2 { 54 | _, _ = fmt.Fprintf(l.writer, "%s: %+v ", kvs[i], kvs[i+1]) 55 | } 56 | _, _ = fmt.Fprintf(l.writer, "\n") 57 | _ = l.writer.Flush() 58 | } 59 | 60 | func (l tabLogSink) Error(err error, msg string, kvs ...any) { 61 | kvs = append(kvs, "error", err) 62 | l.Info(0, msg, kvs...) 63 | } 64 | 65 | func (l tabLogSink) WithName(name string) logr.LogSink { 66 | return &tabLogSink{ 67 | name: l.name + "." + name, 68 | keyValues: l.keyValues, 69 | writer: l.writer, 70 | } 71 | } 72 | 73 | func (l tabLogSink) WithValues(kvs ...any) logr.LogSink { 74 | newMap := make(map[string]any, len(l.keyValues)+len(kvs)/2) 75 | for k, v := range l.keyValues { 76 | newMap[k] = v 77 | } 78 | for i := 0; i < len(kvs); i += 2 { 79 | k := kvs[i].(string) //nolint:forcetypeassert 80 | v := kvs[i+1] 81 | newMap[k] = v 82 | } 83 | return &tabLogSink{ 84 | name: l.name, 85 | keyValues: newMap, 86 | writer: l.writer, 87 | } 88 | } 89 | 90 | // NewTabLogger is the main entry-point to this implementation. App developers 91 | // call this somewhere near main() and thenceforth only deal with logr.Logger. 92 | func NewTabLogger() logr.Logger { 93 | sink := &tabLogSink{ 94 | writer: tabwriter.NewWriter(os.Stderr, 40, 8, 2, '\t', 0), 95 | } 96 | return logr.New(sink) 97 | } 98 | -------------------------------------------------------------------------------- /examples/usage_example.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "math/rand" 22 | "time" 23 | 24 | "github.com/go-logr/logr" 25 | ) 26 | 27 | // This application demonstrates the usage of logger. 28 | // It's a simple reconciliation loop that pretends to 29 | // receive notifications about updates from a some API 30 | // server, make some changes, and then submit updates of 31 | // its own. 32 | 33 | // This uses object-based logging. It's also possible 34 | // (but a bit trickier) to use file-level "base" loggers. 35 | 36 | var objectMap = map[string]Object{ 37 | "obj1": { 38 | Name: "obj1", 39 | Kind: "one", 40 | Details: 33, 41 | }, 42 | "obj2": { 43 | Name: "obj2", 44 | Kind: "two", 45 | Details: "hi", 46 | }, 47 | "obj3": { 48 | Name: "obj3", 49 | Kind: "one", 50 | Details: 1, 51 | }, 52 | } 53 | 54 | // Object is an app construct that might want to be logged. 55 | type Object struct { 56 | Name string 57 | Kind string 58 | Details any 59 | } 60 | 61 | // Client is a simulated client in this example app. 62 | type Client struct { 63 | objects map[string]Object 64 | log logr.Logger 65 | } 66 | 67 | // Get retrieves an object. 68 | func (c *Client) Get(key string) (Object, error) { 69 | c.log.V(1).Info("fetching object", "key", key) 70 | obj, ok := c.objects[key] 71 | if !ok { 72 | return Object{}, fmt.Errorf("no object %s exists", key) 73 | } 74 | c.log.V(1).Info("pretending to deserialize object", "key", key, "json", "[insert real json here]") 75 | return obj, nil 76 | } 77 | 78 | // Save stores an object. 79 | func (c *Client) Save(obj Object) error { 80 | c.log.V(1).Info("saving object", "key", obj.Name, "object", obj) 81 | if rand.Intn(2) == 0 { 82 | return fmt.Errorf("couldn't save to %s", obj.Name) 83 | } 84 | c.log.V(1).Info("pretending to post object", "key", obj.Name, "url", "https://fake.test") 85 | return nil 86 | } 87 | 88 | // WatchNext waits for object updates. 89 | func (c *Client) WatchNext() string { 90 | time.Sleep(2 * time.Second) 91 | 92 | keyInd := rand.Intn(len(c.objects)) 93 | 94 | currInd := 0 95 | for key := range c.objects { 96 | if currInd == keyInd { 97 | return key 98 | } 99 | currInd++ 100 | } 101 | 102 | c.log.Info("watch ended") 103 | return "" 104 | } 105 | 106 | // Controller is the main point of this example. 107 | type Controller struct { 108 | log logr.Logger 109 | expectedKind string 110 | client *Client 111 | } 112 | 113 | // Run starts the example controller. 114 | func (c *Controller) Run() { 115 | c.log.Info("starting reconciliation") 116 | 117 | for key := c.client.WatchNext(); key != ""; key = c.client.WatchNext() { 118 | // we can make more specific loggers if we always want to attach a particular named value 119 | log := c.log.WithValues("key", key) 120 | 121 | // fetch our object 122 | obj, err := c.client.Get(key) 123 | if err != nil { 124 | log.Error(err, "unable to reconcile object") 125 | continue 126 | } 127 | 128 | // make sure it's as expected 129 | if obj.Kind != c.expectedKind { 130 | log.Error(nil, "got object that wasn't expected kind", "actual-kind", obj.Kind, "object", obj) 131 | continue 132 | } 133 | 134 | // always log the object with log messages 135 | log = log.WithValues("object", obj) 136 | log.V(1).Info("reconciling object for key") 137 | 138 | // Do some complicated updates updates 139 | obj.Details = obj.Details.(int) * 2 //nolint:forcetypeassert 140 | 141 | // actually save the updates 142 | log.V(1).Info("updating object", "details", obj.Details) 143 | if err := c.client.Save(obj); err != nil { 144 | log.Error(err, "unable to reconcile object") 145 | } 146 | } 147 | 148 | c.log.Info("stopping reconciliation") 149 | } 150 | 151 | // NewController allocates and initializes a Controller. 152 | func NewController(log logr.Logger, objectKind string) *Controller { 153 | ctrlLogger := log.WithName("controller").WithName(objectKind) 154 | client := &Client{ 155 | log: ctrlLogger.WithName("client"), 156 | objects: objectMap, 157 | } 158 | return &Controller{ 159 | log: ctrlLogger, 160 | expectedKind: objectKind, 161 | client: client, 162 | } 163 | } 164 | 165 | func main() { 166 | // use a fake implementation just for demonstration purposes 167 | log := NewTabLogger() 168 | 169 | // update objects with the "one" kind 170 | ctrl := NewController(log, "one") 171 | 172 | ctrl.Run() 173 | } 174 | -------------------------------------------------------------------------------- /funcr/example/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package main is an example of using funcr. 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | 23 | "github.com/go-logr/logr" 24 | "github.com/go-logr/logr/funcr" 25 | ) 26 | 27 | type e struct { 28 | str string 29 | } 30 | 31 | func (e e) Error() string { 32 | return e.str 33 | } 34 | 35 | func helper(log logr.Logger, msg string) { 36 | helper2(log, msg) 37 | } 38 | 39 | func helper2(log logr.Logger, msg string) { 40 | log.WithCallDepth(2).Info(msg) 41 | } 42 | 43 | func main() { 44 | // logr 45 | log := funcr.NewJSON( 46 | func(arg string) { fmt.Println(arg) }, 47 | funcr.Options{ 48 | LogCaller: funcr.All, 49 | LogTimestamp: true, 50 | Verbosity: 1, 51 | }) 52 | logrExample(log.WithName("logr").WithValues("mode", "funcr")) 53 | 54 | // slog (if possible) 55 | doSlog(log) 56 | } 57 | 58 | func logrExample(log logr.Logger) { 59 | log.Info("hello", "val1", 1, "val2", map[string]int{"k": 1}) 60 | log.V(1).Info("you should see this") 61 | log.V(1).V(1).Info("you should NOT see this") 62 | log.Error(nil, "uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) 63 | log.Error(e{"an error occurred"}, "goodbye", "code", -1) 64 | helper(log, "thru a helper") 65 | } 66 | -------------------------------------------------------------------------------- /funcr/example/main_noslog.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.21 2 | // +build !go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Package main is an example of using funcr. 21 | package main 22 | 23 | import ( 24 | "github.com/go-logr/logr" 25 | ) 26 | 27 | func doSlog(log logr.Logger) { 28 | log.Error(nil, "Sorry, slog is not supported on this version of Go") 29 | } 30 | -------------------------------------------------------------------------------- /funcr/example/main_slog.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Package main is an example of using funcr. 21 | package main 22 | 23 | import ( 24 | "log/slog" 25 | 26 | "github.com/go-logr/logr" 27 | ) 28 | 29 | func doSlog(log logr.Logger) { 30 | slogger := slog.New(logr.ToSlogHandler(log.WithName("slog").WithValues("mode", "slog"))) 31 | slogExample(slogger) 32 | } 33 | 34 | func slogExample(log *slog.Logger) { 35 | log.Warn("hello", "val1", 1, "val2", map[string]int{"k": 1}) 36 | log.Info("you should see this") 37 | log.Debug("you should NOT see this") 38 | log.Error("uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) 39 | log.With("attr1", 1, "attr2", 2).Info("with attrs") 40 | log.WithGroup("groupname").Info("with group", "slog2", false) 41 | log.WithGroup("group1").With("attr1", 1).WithGroup("group2").With("attr2", 2).Info("msg", "arg", "val") 42 | } 43 | -------------------------------------------------------------------------------- /funcr/example_formatter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package funcr_test 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/go-logr/logr" 24 | "github.com/go-logr/logr/funcr" 25 | ) 26 | 27 | // NewStdoutLogger returns a logr.Logger that prints to stdout. 28 | // It demonstrates how to implement a custom With* function which 29 | // controls whether INFO or ERROR are printed in front of the log 30 | // message. 31 | func NewStdoutLogger() logr.Logger { 32 | l := &stdoutlogger{ 33 | Formatter: funcr.NewFormatter(funcr.Options{}), 34 | } 35 | return logr.New(l) 36 | } 37 | 38 | type stdoutlogger struct { 39 | funcr.Formatter 40 | logMsgType bool 41 | } 42 | 43 | func (l stdoutlogger) WithName(name string) logr.LogSink { 44 | l.AddName(name) 45 | return &l 46 | } 47 | 48 | func (l stdoutlogger) WithValues(kvList ...any) logr.LogSink { 49 | l.AddValues(kvList) 50 | return &l 51 | } 52 | 53 | func (l stdoutlogger) WithCallDepth(depth int) logr.LogSink { 54 | l.AddCallDepth(depth) 55 | return &l 56 | } 57 | 58 | func (l stdoutlogger) Info(level int, msg string, kvList ...any) { 59 | prefix, args := l.FormatInfo(level, msg, kvList) 60 | l.write("INFO", prefix, args) 61 | } 62 | 63 | func (l stdoutlogger) Error(err error, msg string, kvList ...any) { 64 | prefix, args := l.FormatError(err, msg, kvList) 65 | l.write("ERROR", prefix, args) 66 | } 67 | 68 | func (l stdoutlogger) write(msgType, prefix, args string) { 69 | var parts []string 70 | if l.logMsgType { 71 | parts = append(parts, msgType) 72 | } 73 | if prefix != "" { 74 | parts = append(parts, prefix) 75 | } 76 | parts = append(parts, args) 77 | fmt.Println(strings.Join(parts, ": ")) 78 | } 79 | 80 | // WithLogMsgType returns a copy of the logger with new settings for 81 | // logging the message type. It returns the original logger if the 82 | // underlying LogSink is not a stdoutlogger. 83 | func WithLogMsgType(log logr.Logger, logMsgType bool) logr.Logger { 84 | if l, ok := log.GetSink().(*stdoutlogger); ok { 85 | clone := *l 86 | clone.logMsgType = logMsgType 87 | log = log.WithSink(&clone) 88 | } 89 | return log 90 | } 91 | 92 | // Assert conformance to the interfaces. 93 | var _ logr.LogSink = &stdoutlogger{} 94 | var _ logr.CallDepthLogSink = &stdoutlogger{} 95 | 96 | func ExampleFormatter() { 97 | l := NewStdoutLogger() 98 | l.Info("no message type") 99 | WithLogMsgType(l, true).Info("with message type") 100 | // Output: 101 | // "level"=0 "msg"="no message type" 102 | // INFO: "level"=0 "msg"="with message type" 103 | } 104 | -------------------------------------------------------------------------------- /funcr/example_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package funcr_test 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/go-logr/logr/funcr" 23 | ) 24 | 25 | func ExampleNew() { 26 | log := funcr.New(func(prefix, args string) { 27 | fmt.Println(prefix, args) 28 | }, funcr.Options{}) 29 | 30 | log = log.WithName("MyLogger") 31 | log = log.WithValues("savedKey", "savedValue") 32 | log.Info("the message", "key", "value") 33 | // Output: MyLogger "level"=0 "msg"="the message" "savedKey"="savedValue" "key"="value" 34 | } 35 | 36 | func ExampleNewJSON() { 37 | log := funcr.NewJSON(func(obj string) { 38 | fmt.Println(obj) 39 | }, funcr.Options{}) 40 | 41 | log = log.WithName("MyLogger") 42 | log = log.WithValues("savedKey", "savedValue") 43 | log.Info("the message", "key", "value") 44 | // Output: {"logger":"MyLogger","level":0,"msg":"the message","savedKey":"savedValue","key":"value"} 45 | } 46 | 47 | func ExampleUnderlier() { 48 | log := funcr.New(func(prefix, args string) { 49 | fmt.Println(prefix, args) 50 | }, funcr.Options{}) 51 | 52 | if underlier, ok := log.GetSink().(funcr.Underlier); ok { 53 | fn := underlier.GetUnderlying() 54 | fn("hello", "world") 55 | } 56 | // Output: hello world 57 | } 58 | 59 | func ExampleOptions() { 60 | log := funcr.NewJSON( 61 | func(obj string) { fmt.Println(obj) }, 62 | funcr.Options{ 63 | LogCaller: funcr.All, 64 | Verbosity: 1, // V(2) and higher is ignored. 65 | }) 66 | log.V(0).Info("V(0) message", "key", "value") 67 | log.V(1).Info("V(1) message", "key", "value") 68 | log.V(2).Info("V(2) message", "key", "value") 69 | // Output: 70 | // {"logger":"","caller":{"file":"example_test.go","line":66},"level":0,"msg":"V(0) message","key":"value"} 71 | // {"logger":"","caller":{"file":"example_test.go","line":67},"level":1,"msg":"V(1) message","key":"value"} 72 | } 73 | 74 | func ExampleOptions_renderHooks() { 75 | // prefix all builtin keys with "log:" 76 | prefixSpecialKeys := func(kvList []any) []any { 77 | for i := 0; i < len(kvList); i += 2 { 78 | k, _ := kvList[i].(string) 79 | kvList[i] = "log:" + k 80 | } 81 | return kvList 82 | } 83 | 84 | // present saved values as a single JSON object 85 | valuesAsObject := func(kvList []any) []any { 86 | return []any{"labels", funcr.PseudoStruct(kvList)} 87 | } 88 | 89 | log := funcr.NewJSON( 90 | func(obj string) { fmt.Println(obj) }, 91 | funcr.Options{ 92 | RenderBuiltinsHook: prefixSpecialKeys, 93 | RenderValuesHook: valuesAsObject, 94 | }) 95 | log = log.WithName("MyLogger") 96 | log = log.WithValues("savedKey1", "savedVal1") 97 | log = log.WithValues("savedKey2", "savedVal2") 98 | log.Info("the message", "key", "value") 99 | // Output: {"log:logger":"MyLogger","log:level":0,"log:msg":"the message","labels":{"savedKey1":"savedVal1","savedKey2":"savedVal2"},"key":"value"} 100 | } 101 | 102 | func ExamplePseudoStruct() { 103 | log := funcr.NewJSON( 104 | func(obj string) { fmt.Println(obj) }, 105 | funcr.Options{}) 106 | kv := []any{ 107 | "field1", 12345, 108 | "field2", true, 109 | } 110 | log.Info("the message", "key", funcr.PseudoStruct(kv)) 111 | // Output: {"logger":"","level":0,"msg":"the message","key":{"field1":12345,"field2":true}} 112 | } 113 | 114 | func ExampleOptions_maxLogDepth() { 115 | type List struct { 116 | Next *List 117 | } 118 | l := List{} 119 | l.Next = &l // recursive 120 | 121 | log := funcr.NewJSON( 122 | func(obj string) { fmt.Println(obj) }, 123 | funcr.Options{MaxLogDepth: 4}) 124 | log.Info("recursive", "list", l) 125 | // Output: {"logger":"","level":0,"msg":"recursive","list":{"Next":{"Next":{"Next":{"Next":{"Next":""}}}}}} 126 | } 127 | -------------------------------------------------------------------------------- /funcr/funcr.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package funcr implements formatting of structured log messages and 18 | // optionally captures the call site and timestamp. 19 | // 20 | // The simplest way to use it is via its implementation of a 21 | // github.com/go-logr/logr.LogSink with output through an arbitrary 22 | // "write" function. See New and NewJSON for details. 23 | // 24 | // # Custom LogSinks 25 | // 26 | // For users who need more control, a funcr.Formatter can be embedded inside 27 | // your own custom LogSink implementation. This is useful when the LogSink 28 | // needs to implement additional methods, for example. 29 | // 30 | // # Formatting 31 | // 32 | // This will respect logr.Marshaler, fmt.Stringer, and error interfaces for 33 | // values which are being logged. When rendering a struct, funcr will use Go's 34 | // standard JSON tags (all except "string"). 35 | package funcr 36 | 37 | import ( 38 | "bytes" 39 | "encoding" 40 | "encoding/json" 41 | "fmt" 42 | "path/filepath" 43 | "reflect" 44 | "runtime" 45 | "strconv" 46 | "strings" 47 | "time" 48 | 49 | "github.com/go-logr/logr" 50 | ) 51 | 52 | // New returns a logr.Logger which is implemented by an arbitrary function. 53 | func New(fn func(prefix, args string), opts Options) logr.Logger { 54 | return logr.New(newSink(fn, NewFormatter(opts))) 55 | } 56 | 57 | // NewJSON returns a logr.Logger which is implemented by an arbitrary function 58 | // and produces JSON output. 59 | func NewJSON(fn func(obj string), opts Options) logr.Logger { 60 | fnWrapper := func(_, obj string) { 61 | fn(obj) 62 | } 63 | return logr.New(newSink(fnWrapper, NewFormatterJSON(opts))) 64 | } 65 | 66 | // Underlier exposes access to the underlying logging function. Since 67 | // callers only have a logr.Logger, they have to know which 68 | // implementation is in use, so this interface is less of an 69 | // abstraction and more of a way to test type conversion. 70 | type Underlier interface { 71 | GetUnderlying() func(prefix, args string) 72 | } 73 | 74 | func newSink(fn func(prefix, args string), formatter Formatter) logr.LogSink { 75 | l := &fnlogger{ 76 | Formatter: formatter, 77 | write: fn, 78 | } 79 | // For skipping fnlogger.Info and fnlogger.Error. 80 | l.AddCallDepth(1) // via Formatter 81 | return l 82 | } 83 | 84 | // Options carries parameters which influence the way logs are generated. 85 | type Options struct { 86 | // LogCaller tells funcr to add a "caller" key to some or all log lines. 87 | // This has some overhead, so some users might not want it. 88 | LogCaller MessageClass 89 | 90 | // LogCallerFunc tells funcr to also log the calling function name. This 91 | // has no effect if caller logging is not enabled (see Options.LogCaller). 92 | LogCallerFunc bool 93 | 94 | // LogTimestamp tells funcr to add a "ts" key to log lines. This has some 95 | // overhead, so some users might not want it. 96 | LogTimestamp bool 97 | 98 | // TimestampFormat tells funcr how to render timestamps when LogTimestamp 99 | // is enabled. If not specified, a default format will be used. For more 100 | // details, see docs for Go's time.Layout. 101 | TimestampFormat string 102 | 103 | // LogInfoLevel tells funcr what key to use to log the info level. 104 | // If not specified, the info level will be logged as "level". 105 | // If this is set to "", the info level will not be logged at all. 106 | LogInfoLevel *string 107 | 108 | // Verbosity tells funcr which V logs to produce. Higher values enable 109 | // more logs. Info logs at or below this level will be written, while logs 110 | // above this level will be discarded. 111 | Verbosity int 112 | 113 | // RenderBuiltinsHook allows users to mutate the list of key-value pairs 114 | // while a log line is being rendered. The kvList argument follows logr 115 | // conventions - each pair of slice elements is comprised of a string key 116 | // and an arbitrary value (verified and sanitized before calling this 117 | // hook). The value returned must follow the same conventions. This hook 118 | // can be used to audit or modify logged data. For example, you might want 119 | // to prefix all of funcr's built-in keys with some string. This hook is 120 | // only called for built-in (provided by funcr itself) key-value pairs. 121 | // Equivalent hooks are offered for key-value pairs saved via 122 | // logr.Logger.WithValues or Formatter.AddValues (see RenderValuesHook) and 123 | // for user-provided pairs (see RenderArgsHook). 124 | RenderBuiltinsHook func(kvList []any) []any 125 | 126 | // RenderValuesHook is the same as RenderBuiltinsHook, except that it is 127 | // only called for key-value pairs saved via logr.Logger.WithValues. See 128 | // RenderBuiltinsHook for more details. 129 | RenderValuesHook func(kvList []any) []any 130 | 131 | // RenderArgsHook is the same as RenderBuiltinsHook, except that it is only 132 | // called for key-value pairs passed directly to Info and Error. See 133 | // RenderBuiltinsHook for more details. 134 | RenderArgsHook func(kvList []any) []any 135 | 136 | // MaxLogDepth tells funcr how many levels of nested fields (e.g. a struct 137 | // that contains a struct, etc.) it may log. Every time it finds a struct, 138 | // slice, array, or map the depth is increased by one. When the maximum is 139 | // reached, the value will be converted to a string indicating that the max 140 | // depth has been exceeded. If this field is not specified, a default 141 | // value will be used. 142 | MaxLogDepth int 143 | } 144 | 145 | // MessageClass indicates which category or categories of messages to consider. 146 | type MessageClass int 147 | 148 | const ( 149 | // None ignores all message classes. 150 | None MessageClass = iota 151 | // All considers all message classes. 152 | All 153 | // Info only considers info messages. 154 | Info 155 | // Error only considers error messages. 156 | Error 157 | ) 158 | 159 | // fnlogger inherits some of its LogSink implementation from Formatter 160 | // and just needs to add some glue code. 161 | type fnlogger struct { 162 | Formatter 163 | write func(prefix, args string) 164 | } 165 | 166 | func (l fnlogger) WithName(name string) logr.LogSink { 167 | l.AddName(name) // via Formatter 168 | return &l 169 | } 170 | 171 | func (l fnlogger) WithValues(kvList ...any) logr.LogSink { 172 | l.AddValues(kvList) // via Formatter 173 | return &l 174 | } 175 | 176 | func (l fnlogger) WithCallDepth(depth int) logr.LogSink { 177 | l.AddCallDepth(depth) // via Formatter 178 | return &l 179 | } 180 | 181 | func (l fnlogger) Info(level int, msg string, kvList ...any) { 182 | prefix, args := l.FormatInfo(level, msg, kvList) 183 | l.write(prefix, args) 184 | } 185 | 186 | func (l fnlogger) Error(err error, msg string, kvList ...any) { 187 | prefix, args := l.FormatError(err, msg, kvList) 188 | l.write(prefix, args) 189 | } 190 | 191 | func (l fnlogger) GetUnderlying() func(prefix, args string) { 192 | return l.write 193 | } 194 | 195 | // Assert conformance to the interfaces. 196 | var _ logr.LogSink = &fnlogger{} 197 | var _ logr.CallDepthLogSink = &fnlogger{} 198 | var _ Underlier = &fnlogger{} 199 | 200 | // NewFormatter constructs a Formatter which emits a JSON-like key=value format. 201 | func NewFormatter(opts Options) Formatter { 202 | return newFormatter(opts, outputKeyValue) 203 | } 204 | 205 | // NewFormatterJSON constructs a Formatter which emits strict JSON. 206 | func NewFormatterJSON(opts Options) Formatter { 207 | return newFormatter(opts, outputJSON) 208 | } 209 | 210 | // Defaults for Options. 211 | const defaultTimestampFormat = "2006-01-02 15:04:05.000000" 212 | const defaultMaxLogDepth = 16 213 | 214 | func newFormatter(opts Options, outfmt outputFormat) Formatter { 215 | if opts.TimestampFormat == "" { 216 | opts.TimestampFormat = defaultTimestampFormat 217 | } 218 | if opts.MaxLogDepth == 0 { 219 | opts.MaxLogDepth = defaultMaxLogDepth 220 | } 221 | if opts.LogInfoLevel == nil { 222 | opts.LogInfoLevel = new(string) 223 | *opts.LogInfoLevel = "level" 224 | } 225 | f := Formatter{ 226 | outputFormat: outfmt, 227 | prefix: "", 228 | values: nil, 229 | depth: 0, 230 | opts: &opts, 231 | } 232 | return f 233 | } 234 | 235 | // Formatter is an opaque struct which can be embedded in a LogSink 236 | // implementation. It should be constructed with NewFormatter. Some of 237 | // its methods directly implement logr.LogSink. 238 | type Formatter struct { 239 | outputFormat outputFormat 240 | prefix string 241 | values []any 242 | valuesStr string 243 | depth int 244 | opts *Options 245 | groupName string // for slog groups 246 | groups []groupDef 247 | } 248 | 249 | // outputFormat indicates which outputFormat to use. 250 | type outputFormat int 251 | 252 | const ( 253 | // outputKeyValue emits a JSON-like key=value format, but not strict JSON. 254 | outputKeyValue outputFormat = iota 255 | // outputJSON emits strict JSON. 256 | outputJSON 257 | ) 258 | 259 | // groupDef represents a saved group. The values may be empty, but we don't 260 | // know if we need to render the group until the final record is rendered. 261 | type groupDef struct { 262 | name string 263 | values string 264 | } 265 | 266 | // PseudoStruct is a list of key-value pairs that gets logged as a struct. 267 | type PseudoStruct []any 268 | 269 | // render produces a log line, ready to use. 270 | func (f Formatter) render(builtins, args []any) string { 271 | // Empirically bytes.Buffer is faster than strings.Builder for this. 272 | buf := bytes.NewBuffer(make([]byte, 0, 1024)) 273 | 274 | if f.outputFormat == outputJSON { 275 | buf.WriteByte('{') // for the whole record 276 | } 277 | 278 | // Render builtins 279 | vals := builtins 280 | if hook := f.opts.RenderBuiltinsHook; hook != nil { 281 | vals = hook(f.sanitize(vals)) 282 | } 283 | f.flatten(buf, vals, false) // keys are ours, no need to escape 284 | continuing := len(builtins) > 0 285 | 286 | // Turn the inner-most group into a string 287 | argsStr := func() string { 288 | buf := bytes.NewBuffer(make([]byte, 0, 1024)) 289 | 290 | vals = args 291 | if hook := f.opts.RenderArgsHook; hook != nil { 292 | vals = hook(f.sanitize(vals)) 293 | } 294 | f.flatten(buf, vals, true) // escape user-provided keys 295 | 296 | return buf.String() 297 | }() 298 | 299 | // Render the stack of groups from the inside out. 300 | bodyStr := f.renderGroup(f.groupName, f.valuesStr, argsStr) 301 | for i := len(f.groups) - 1; i >= 0; i-- { 302 | grp := &f.groups[i] 303 | if grp.values == "" && bodyStr == "" { 304 | // no contents, so we must elide the whole group 305 | continue 306 | } 307 | bodyStr = f.renderGroup(grp.name, grp.values, bodyStr) 308 | } 309 | 310 | if bodyStr != "" { 311 | if continuing { 312 | buf.WriteByte(f.comma()) 313 | } 314 | buf.WriteString(bodyStr) 315 | } 316 | 317 | if f.outputFormat == outputJSON { 318 | buf.WriteByte('}') // for the whole record 319 | } 320 | 321 | return buf.String() 322 | } 323 | 324 | // renderGroup returns a string representation of the named group with rendered 325 | // values and args. If the name is empty, this will return the values and args, 326 | // joined. If the name is not empty, this will return a single key-value pair, 327 | // where the value is a grouping of the values and args. If the values and 328 | // args are both empty, this will return an empty string, even if the name was 329 | // specified. 330 | func (f Formatter) renderGroup(name string, values string, args string) string { 331 | buf := bytes.NewBuffer(make([]byte, 0, 1024)) 332 | 333 | needClosingBrace := false 334 | if name != "" && (values != "" || args != "") { 335 | buf.WriteString(f.quoted(name, true)) // escape user-provided keys 336 | buf.WriteByte(f.colon()) 337 | buf.WriteByte('{') 338 | needClosingBrace = true 339 | } 340 | 341 | continuing := false 342 | if values != "" { 343 | buf.WriteString(values) 344 | continuing = true 345 | } 346 | 347 | if args != "" { 348 | if continuing { 349 | buf.WriteByte(f.comma()) 350 | } 351 | buf.WriteString(args) 352 | } 353 | 354 | if needClosingBrace { 355 | buf.WriteByte('}') 356 | } 357 | 358 | return buf.String() 359 | } 360 | 361 | // flatten renders a list of key-value pairs into a buffer. If escapeKeys is 362 | // true, the keys are assumed to have non-JSON-compatible characters in them 363 | // and must be evaluated for escapes. 364 | // 365 | // This function returns a potentially modified version of kvList, which 366 | // ensures that there is a value for every key (adding a value if needed) and 367 | // that each key is a string (substituting a key if needed). 368 | func (f Formatter) flatten(buf *bytes.Buffer, kvList []any, escapeKeys bool) []any { 369 | // This logic overlaps with sanitize() but saves one type-cast per key, 370 | // which can be measurable. 371 | if len(kvList)%2 != 0 { 372 | kvList = append(kvList, noValue) 373 | } 374 | copied := false 375 | for i := 0; i < len(kvList); i += 2 { 376 | k, ok := kvList[i].(string) 377 | if !ok { 378 | if !copied { 379 | newList := make([]any, len(kvList)) 380 | copy(newList, kvList) 381 | kvList = newList 382 | copied = true 383 | } 384 | k = f.nonStringKey(kvList[i]) 385 | kvList[i] = k 386 | } 387 | v := kvList[i+1] 388 | 389 | if i > 0 { 390 | if f.outputFormat == outputJSON { 391 | buf.WriteByte(f.comma()) 392 | } else { 393 | // In theory the format could be something we don't understand. In 394 | // practice, we control it, so it won't be. 395 | buf.WriteByte(' ') 396 | } 397 | } 398 | 399 | buf.WriteString(f.quoted(k, escapeKeys)) 400 | buf.WriteByte(f.colon()) 401 | buf.WriteString(f.pretty(v)) 402 | } 403 | return kvList 404 | } 405 | 406 | func (f Formatter) quoted(str string, escape bool) string { 407 | if escape { 408 | return prettyString(str) 409 | } 410 | // this is faster 411 | return `"` + str + `"` 412 | } 413 | 414 | func (f Formatter) comma() byte { 415 | if f.outputFormat == outputJSON { 416 | return ',' 417 | } 418 | return ' ' 419 | } 420 | 421 | func (f Formatter) colon() byte { 422 | if f.outputFormat == outputJSON { 423 | return ':' 424 | } 425 | return '=' 426 | } 427 | 428 | func (f Formatter) pretty(value any) string { 429 | return f.prettyWithFlags(value, 0, 0) 430 | } 431 | 432 | const ( 433 | flagRawStruct = 0x1 // do not print braces on structs 434 | ) 435 | 436 | // TODO: This is not fast. Most of the overhead goes here. 437 | func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string { 438 | if depth > f.opts.MaxLogDepth { 439 | return `""` 440 | } 441 | 442 | // Handle types that take full control of logging. 443 | if v, ok := value.(logr.Marshaler); ok { 444 | // Replace the value with what the type wants to get logged. 445 | // That then gets handled below via reflection. 446 | value = invokeMarshaler(v) 447 | } 448 | 449 | // Handle types that want to format themselves. 450 | switch v := value.(type) { 451 | case fmt.Stringer: 452 | value = invokeStringer(v) 453 | case error: 454 | value = invokeError(v) 455 | } 456 | 457 | // Handling the most common types without reflect is a small perf win. 458 | switch v := value.(type) { 459 | case bool: 460 | return strconv.FormatBool(v) 461 | case string: 462 | return prettyString(v) 463 | case int: 464 | return strconv.FormatInt(int64(v), 10) 465 | case int8: 466 | return strconv.FormatInt(int64(v), 10) 467 | case int16: 468 | return strconv.FormatInt(int64(v), 10) 469 | case int32: 470 | return strconv.FormatInt(int64(v), 10) 471 | case int64: 472 | return strconv.FormatInt(int64(v), 10) 473 | case uint: 474 | return strconv.FormatUint(uint64(v), 10) 475 | case uint8: 476 | return strconv.FormatUint(uint64(v), 10) 477 | case uint16: 478 | return strconv.FormatUint(uint64(v), 10) 479 | case uint32: 480 | return strconv.FormatUint(uint64(v), 10) 481 | case uint64: 482 | return strconv.FormatUint(v, 10) 483 | case uintptr: 484 | return strconv.FormatUint(uint64(v), 10) 485 | case float32: 486 | return strconv.FormatFloat(float64(v), 'f', -1, 32) 487 | case float64: 488 | return strconv.FormatFloat(v, 'f', -1, 64) 489 | case complex64: 490 | return `"` + strconv.FormatComplex(complex128(v), 'f', -1, 64) + `"` 491 | case complex128: 492 | return `"` + strconv.FormatComplex(v, 'f', -1, 128) + `"` 493 | case PseudoStruct: 494 | buf := bytes.NewBuffer(make([]byte, 0, 1024)) 495 | v = f.sanitize(v) 496 | if flags&flagRawStruct == 0 { 497 | buf.WriteByte('{') 498 | } 499 | for i := 0; i < len(v); i += 2 { 500 | if i > 0 { 501 | buf.WriteByte(f.comma()) 502 | } 503 | k, _ := v[i].(string) // sanitize() above means no need to check success 504 | // arbitrary keys might need escaping 505 | buf.WriteString(prettyString(k)) 506 | buf.WriteByte(f.colon()) 507 | buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1)) 508 | } 509 | if flags&flagRawStruct == 0 { 510 | buf.WriteByte('}') 511 | } 512 | return buf.String() 513 | } 514 | 515 | buf := bytes.NewBuffer(make([]byte, 0, 256)) 516 | t := reflect.TypeOf(value) 517 | if t == nil { 518 | return "null" 519 | } 520 | v := reflect.ValueOf(value) 521 | switch t.Kind() { 522 | case reflect.Bool: 523 | return strconv.FormatBool(v.Bool()) 524 | case reflect.String: 525 | return prettyString(v.String()) 526 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 527 | return strconv.FormatInt(int64(v.Int()), 10) 528 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 529 | return strconv.FormatUint(uint64(v.Uint()), 10) 530 | case reflect.Float32: 531 | return strconv.FormatFloat(float64(v.Float()), 'f', -1, 32) 532 | case reflect.Float64: 533 | return strconv.FormatFloat(v.Float(), 'f', -1, 64) 534 | case reflect.Complex64: 535 | return `"` + strconv.FormatComplex(complex128(v.Complex()), 'f', -1, 64) + `"` 536 | case reflect.Complex128: 537 | return `"` + strconv.FormatComplex(v.Complex(), 'f', -1, 128) + `"` 538 | case reflect.Struct: 539 | if flags&flagRawStruct == 0 { 540 | buf.WriteByte('{') 541 | } 542 | printComma := false // testing i>0 is not enough because of JSON omitted fields 543 | for i := 0; i < t.NumField(); i++ { 544 | fld := t.Field(i) 545 | if fld.PkgPath != "" { 546 | // reflect says this field is only defined for non-exported fields. 547 | continue 548 | } 549 | if !v.Field(i).CanInterface() { 550 | // reflect isn't clear exactly what this means, but we can't use it. 551 | continue 552 | } 553 | name := "" 554 | omitempty := false 555 | if tag, found := fld.Tag.Lookup("json"); found { 556 | if tag == "-" { 557 | continue 558 | } 559 | if comma := strings.Index(tag, ","); comma != -1 { 560 | if n := tag[:comma]; n != "" { 561 | name = n 562 | } 563 | rest := tag[comma:] 564 | if strings.Contains(rest, ",omitempty,") || strings.HasSuffix(rest, ",omitempty") { 565 | omitempty = true 566 | } 567 | } else { 568 | name = tag 569 | } 570 | } 571 | if omitempty && isEmpty(v.Field(i)) { 572 | continue 573 | } 574 | if printComma { 575 | buf.WriteByte(f.comma()) 576 | } 577 | printComma = true // if we got here, we are rendering a field 578 | if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" { 579 | buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1)) 580 | continue 581 | } 582 | if name == "" { 583 | name = fld.Name 584 | } 585 | // field names can't contain characters which need escaping 586 | buf.WriteString(f.quoted(name, false)) 587 | buf.WriteByte(f.colon()) 588 | buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1)) 589 | } 590 | if flags&flagRawStruct == 0 { 591 | buf.WriteByte('}') 592 | } 593 | return buf.String() 594 | case reflect.Slice, reflect.Array: 595 | // If this is outputing as JSON make sure this isn't really a json.RawMessage. 596 | // If so just emit "as-is" and don't pretty it as that will just print 597 | // it as [X,Y,Z,...] which isn't terribly useful vs the string form you really want. 598 | if f.outputFormat == outputJSON { 599 | if rm, ok := value.(json.RawMessage); ok { 600 | // If it's empty make sure we emit an empty value as the array style would below. 601 | if len(rm) > 0 { 602 | buf.Write(rm) 603 | } else { 604 | buf.WriteString("null") 605 | } 606 | return buf.String() 607 | } 608 | } 609 | buf.WriteByte('[') 610 | for i := 0; i < v.Len(); i++ { 611 | if i > 0 { 612 | buf.WriteByte(f.comma()) 613 | } 614 | e := v.Index(i) 615 | buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1)) 616 | } 617 | buf.WriteByte(']') 618 | return buf.String() 619 | case reflect.Map: 620 | buf.WriteByte('{') 621 | // This does not sort the map keys, for best perf. 622 | it := v.MapRange() 623 | i := 0 624 | for it.Next() { 625 | if i > 0 { 626 | buf.WriteByte(f.comma()) 627 | } 628 | // If a map key supports TextMarshaler, use it. 629 | keystr := "" 630 | if m, ok := it.Key().Interface().(encoding.TextMarshaler); ok { 631 | txt, err := m.MarshalText() 632 | if err != nil { 633 | keystr = fmt.Sprintf("", err.Error()) 634 | } else { 635 | keystr = string(txt) 636 | } 637 | keystr = prettyString(keystr) 638 | } else { 639 | // prettyWithFlags will produce already-escaped values 640 | keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1) 641 | if t.Key().Kind() != reflect.String { 642 | // JSON only does string keys. Unlike Go's standard JSON, we'll 643 | // convert just about anything to a string. 644 | keystr = prettyString(keystr) 645 | } 646 | } 647 | buf.WriteString(keystr) 648 | buf.WriteByte(f.colon()) 649 | buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1)) 650 | i++ 651 | } 652 | buf.WriteByte('}') 653 | return buf.String() 654 | case reflect.Ptr, reflect.Interface: 655 | if v.IsNil() { 656 | return "null" 657 | } 658 | return f.prettyWithFlags(v.Elem().Interface(), 0, depth) 659 | } 660 | return fmt.Sprintf(`""`, t.Kind().String()) 661 | } 662 | 663 | func prettyString(s string) string { 664 | // Avoid escaping (which does allocations) if we can. 665 | if needsEscape(s) { 666 | return strconv.Quote(s) 667 | } 668 | b := bytes.NewBuffer(make([]byte, 0, 1024)) 669 | b.WriteByte('"') 670 | b.WriteString(s) 671 | b.WriteByte('"') 672 | return b.String() 673 | } 674 | 675 | // needsEscape determines whether the input string needs to be escaped or not, 676 | // without doing any allocations. 677 | func needsEscape(s string) bool { 678 | for _, r := range s { 679 | if !strconv.IsPrint(r) || r == '\\' || r == '"' { 680 | return true 681 | } 682 | } 683 | return false 684 | } 685 | 686 | func isEmpty(v reflect.Value) bool { 687 | switch v.Kind() { 688 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 689 | return v.Len() == 0 690 | case reflect.Bool: 691 | return !v.Bool() 692 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 693 | return v.Int() == 0 694 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 695 | return v.Uint() == 0 696 | case reflect.Float32, reflect.Float64: 697 | return v.Float() == 0 698 | case reflect.Complex64, reflect.Complex128: 699 | return v.Complex() == 0 700 | case reflect.Interface, reflect.Ptr: 701 | return v.IsNil() 702 | } 703 | return false 704 | } 705 | 706 | func invokeMarshaler(m logr.Marshaler) (ret any) { 707 | defer func() { 708 | if r := recover(); r != nil { 709 | ret = fmt.Sprintf("", r) 710 | } 711 | }() 712 | return m.MarshalLog() 713 | } 714 | 715 | func invokeStringer(s fmt.Stringer) (ret string) { 716 | defer func() { 717 | if r := recover(); r != nil { 718 | ret = fmt.Sprintf("", r) 719 | } 720 | }() 721 | return s.String() 722 | } 723 | 724 | func invokeError(e error) (ret string) { 725 | defer func() { 726 | if r := recover(); r != nil { 727 | ret = fmt.Sprintf("", r) 728 | } 729 | }() 730 | return e.Error() 731 | } 732 | 733 | // Caller represents the original call site for a log line, after considering 734 | // logr.Logger.WithCallDepth and logr.Logger.WithCallStackHelper. The File and 735 | // Line fields will always be provided, while the Func field is optional. 736 | // Users can set the render hook fields in Options to examine logged key-value 737 | // pairs, one of which will be {"caller", Caller} if the Options.LogCaller 738 | // field is enabled for the given MessageClass. 739 | type Caller struct { 740 | // File is the basename of the file for this call site. 741 | File string `json:"file"` 742 | // Line is the line number in the file for this call site. 743 | Line int `json:"line"` 744 | // Func is the function name for this call site, or empty if 745 | // Options.LogCallerFunc is not enabled. 746 | Func string `json:"function,omitempty"` 747 | } 748 | 749 | func (f Formatter) caller() Caller { 750 | // +1 for this frame, +1 for Info/Error. 751 | pc, file, line, ok := runtime.Caller(f.depth + 2) 752 | if !ok { 753 | return Caller{"", 0, ""} 754 | } 755 | fn := "" 756 | if f.opts.LogCallerFunc { 757 | if fp := runtime.FuncForPC(pc); fp != nil { 758 | fn = fp.Name() 759 | } 760 | } 761 | 762 | return Caller{filepath.Base(file), line, fn} 763 | } 764 | 765 | const noValue = "" 766 | 767 | func (f Formatter) nonStringKey(v any) string { 768 | return fmt.Sprintf("", f.snippet(v)) 769 | } 770 | 771 | // snippet produces a short snippet string of an arbitrary value. 772 | func (f Formatter) snippet(v any) string { 773 | const snipLen = 16 774 | 775 | snip := f.pretty(v) 776 | if len(snip) > snipLen { 777 | snip = snip[:snipLen] 778 | } 779 | return snip 780 | } 781 | 782 | // sanitize ensures that a list of key-value pairs has a value for every key 783 | // (adding a value if needed) and that each key is a string (substituting a key 784 | // if needed). 785 | func (f Formatter) sanitize(kvList []any) []any { 786 | if len(kvList)%2 != 0 { 787 | kvList = append(kvList, noValue) 788 | } 789 | for i := 0; i < len(kvList); i += 2 { 790 | _, ok := kvList[i].(string) 791 | if !ok { 792 | kvList[i] = f.nonStringKey(kvList[i]) 793 | } 794 | } 795 | return kvList 796 | } 797 | 798 | // startGroup opens a new group scope (basically a sub-struct), which locks all 799 | // the current saved values and starts them anew. This is needed to satisfy 800 | // slog. 801 | func (f *Formatter) startGroup(name string) { 802 | // Unnamed groups are just inlined. 803 | if name == "" { 804 | return 805 | } 806 | 807 | n := len(f.groups) 808 | f.groups = append(f.groups[:n:n], groupDef{f.groupName, f.valuesStr}) 809 | 810 | // Start collecting new values. 811 | f.groupName = name 812 | f.valuesStr = "" 813 | f.values = nil 814 | } 815 | 816 | // Init configures this Formatter from runtime info, such as the call depth 817 | // imposed by logr itself. 818 | // Note that this receiver is a pointer, so depth can be saved. 819 | func (f *Formatter) Init(info logr.RuntimeInfo) { 820 | f.depth += info.CallDepth 821 | } 822 | 823 | // Enabled checks whether an info message at the given level should be logged. 824 | func (f Formatter) Enabled(level int) bool { 825 | return level <= f.opts.Verbosity 826 | } 827 | 828 | // GetDepth returns the current depth of this Formatter. This is useful for 829 | // implementations which do their own caller attribution. 830 | func (f Formatter) GetDepth() int { 831 | return f.depth 832 | } 833 | 834 | // FormatInfo renders an Info log message into strings. The prefix will be 835 | // empty when no names were set (via AddNames), or when the output is 836 | // configured for JSON. 837 | func (f Formatter) FormatInfo(level int, msg string, kvList []any) (prefix, argsStr string) { 838 | args := make([]any, 0, 64) // using a constant here impacts perf 839 | prefix = f.prefix 840 | if f.outputFormat == outputJSON { 841 | args = append(args, "logger", prefix) 842 | prefix = "" 843 | } 844 | if f.opts.LogTimestamp { 845 | args = append(args, "ts", time.Now().Format(f.opts.TimestampFormat)) 846 | } 847 | if policy := f.opts.LogCaller; policy == All || policy == Info { 848 | args = append(args, "caller", f.caller()) 849 | } 850 | if key := *f.opts.LogInfoLevel; key != "" { 851 | args = append(args, key, level) 852 | } 853 | args = append(args, "msg", msg) 854 | return prefix, f.render(args, kvList) 855 | } 856 | 857 | // FormatError renders an Error log message into strings. The prefix will be 858 | // empty when no names were set (via AddNames), or when the output is 859 | // configured for JSON. 860 | func (f Formatter) FormatError(err error, msg string, kvList []any) (prefix, argsStr string) { 861 | args := make([]any, 0, 64) // using a constant here impacts perf 862 | prefix = f.prefix 863 | if f.outputFormat == outputJSON { 864 | args = append(args, "logger", prefix) 865 | prefix = "" 866 | } 867 | if f.opts.LogTimestamp { 868 | args = append(args, "ts", time.Now().Format(f.opts.TimestampFormat)) 869 | } 870 | if policy := f.opts.LogCaller; policy == All || policy == Error { 871 | args = append(args, "caller", f.caller()) 872 | } 873 | args = append(args, "msg", msg) 874 | var loggableErr any 875 | if err != nil { 876 | loggableErr = err.Error() 877 | } 878 | args = append(args, "error", loggableErr) 879 | return prefix, f.render(args, kvList) 880 | } 881 | 882 | // AddName appends the specified name. funcr uses '/' characters to separate 883 | // name elements. Callers should not pass '/' in the provided name string, but 884 | // this library does not actually enforce that. 885 | func (f *Formatter) AddName(name string) { 886 | if len(f.prefix) > 0 { 887 | f.prefix += "/" 888 | } 889 | f.prefix += name 890 | } 891 | 892 | // AddValues adds key-value pairs to the set of saved values to be logged with 893 | // each log line. 894 | func (f *Formatter) AddValues(kvList []any) { 895 | // Three slice args forces a copy. 896 | n := len(f.values) 897 | f.values = append(f.values[:n:n], kvList...) 898 | 899 | vals := f.values 900 | if hook := f.opts.RenderValuesHook; hook != nil { 901 | vals = hook(f.sanitize(vals)) 902 | } 903 | 904 | // Pre-render values, so we don't have to do it on each Info/Error call. 905 | buf := bytes.NewBuffer(make([]byte, 0, 1024)) 906 | f.flatten(buf, vals, true) // escape user-provided keys 907 | f.valuesStr = buf.String() 908 | } 909 | 910 | // AddCallDepth increases the number of stack-frames to skip when attributing 911 | // the log line to a file and line. 912 | func (f *Formatter) AddCallDepth(depth int) { 913 | f.depth += depth 914 | } 915 | -------------------------------------------------------------------------------- /funcr/slogsink.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package funcr 21 | 22 | import ( 23 | "context" 24 | "log/slog" 25 | 26 | "github.com/go-logr/logr" 27 | ) 28 | 29 | var _ logr.SlogSink = &fnlogger{} 30 | 31 | const extraSlogSinkDepth = 3 // 2 for slog, 1 for SlogSink 32 | 33 | func (l fnlogger) Handle(_ context.Context, record slog.Record) error { 34 | kvList := make([]any, 0, 2*record.NumAttrs()) 35 | record.Attrs(func(attr slog.Attr) bool { 36 | kvList = attrToKVs(attr, kvList) 37 | return true 38 | }) 39 | 40 | if record.Level >= slog.LevelError { 41 | l.WithCallDepth(extraSlogSinkDepth).Error(nil, record.Message, kvList...) 42 | } else { 43 | level := l.levelFromSlog(record.Level) 44 | l.WithCallDepth(extraSlogSinkDepth).Info(level, record.Message, kvList...) 45 | } 46 | return nil 47 | } 48 | 49 | func (l fnlogger) WithAttrs(attrs []slog.Attr) logr.SlogSink { 50 | kvList := make([]any, 0, 2*len(attrs)) 51 | for _, attr := range attrs { 52 | kvList = attrToKVs(attr, kvList) 53 | } 54 | l.AddValues(kvList) 55 | return &l 56 | } 57 | 58 | func (l fnlogger) WithGroup(name string) logr.SlogSink { 59 | l.startGroup(name) 60 | return &l 61 | } 62 | 63 | // attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups 64 | // and other details of slog. 65 | func attrToKVs(attr slog.Attr, kvList []any) []any { 66 | attrVal := attr.Value.Resolve() 67 | if attrVal.Kind() == slog.KindGroup { 68 | groupVal := attrVal.Group() 69 | grpKVs := make([]any, 0, 2*len(groupVal)) 70 | for _, attr := range groupVal { 71 | grpKVs = attrToKVs(attr, grpKVs) 72 | } 73 | if attr.Key == "" { 74 | // slog says we have to inline these 75 | kvList = append(kvList, grpKVs...) 76 | } else { 77 | kvList = append(kvList, attr.Key, PseudoStruct(grpKVs)) 78 | } 79 | } else if attr.Key != "" { 80 | kvList = append(kvList, attr.Key, attrVal.Any()) 81 | } 82 | 83 | return kvList 84 | } 85 | 86 | // levelFromSlog adjusts the level by the logger's verbosity and negates it. 87 | // It ensures that the result is >= 0. This is necessary because the result is 88 | // passed to a LogSink and that API did not historically document whether 89 | // levels could be negative or what that meant. 90 | // 91 | // Some example usage: 92 | // 93 | // logrV0 := getMyLogger() 94 | // logrV2 := logrV0.V(2) 95 | // slogV2 := slog.New(logr.ToSlogHandler(logrV2)) 96 | // slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6) 97 | // slogV2.Info("msg") // =~ logrV2.V(0) =~ logrV0.V(2) 98 | // slogv2.Warn("msg") // =~ logrV2.V(-4) =~ logrV0.V(0) 99 | func (l fnlogger) levelFromSlog(level slog.Level) int { 100 | result := -level 101 | if result < 0 { 102 | result = 0 // because LogSink doesn't expect negative V levels 103 | } 104 | return int(result) 105 | } 106 | -------------------------------------------------------------------------------- /funcr/slogsink_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2021 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package funcr 21 | 22 | import ( 23 | "bytes" 24 | "fmt" 25 | "log/slog" 26 | "path/filepath" 27 | "runtime" 28 | "testing" 29 | 30 | "github.com/go-logr/logr" 31 | "github.com/go-logr/logr/internal/testhelp" 32 | ) 33 | 34 | func TestSlogSink(t *testing.T) { 35 | testCases := []struct { 36 | name string 37 | withAttrs []any 38 | withGroup string 39 | args []any 40 | expect string 41 | }{{ 42 | name: "just msg", 43 | args: makeKV(), 44 | expect: `{"logger":"","level":0,"msg":"msg"}`, 45 | }, { 46 | name: "primitives", 47 | args: makeKV("int", 1, "str", "ABC", "bool", true), 48 | expect: `{"logger":"","level":0,"msg":"msg","int":1,"str":"ABC","bool":true}`, 49 | }, { 50 | name: "with attrs", 51 | withAttrs: makeKV("attrInt", 1, "attrStr", "ABC", "attrBool", true), 52 | args: makeKV("int", 2), 53 | expect: `{"logger":"","level":0,"msg":"msg","attrInt":1,"attrStr":"ABC","attrBool":true,"int":2}`, 54 | }, { 55 | name: "with group", 56 | withGroup: "groupname", 57 | args: makeKV("int", 1, "str", "ABC", "bool", true), 58 | expect: `{"logger":"","level":0,"msg":"msg","groupname":{"int":1,"str":"ABC","bool":true}}`, 59 | }, { 60 | name: "with attrs and group", 61 | withAttrs: makeKV("attrInt", 1, "attrStr", "ABC"), 62 | withGroup: "groupname", 63 | args: makeKV("int", 3, "bool", true), 64 | expect: `{"logger":"","level":0,"msg":"msg","attrInt":1,"attrStr":"ABC","groupname":{"int":3,"bool":true}}`, 65 | }} 66 | 67 | for _, tc := range testCases { 68 | t.Run(tc.name, func(t *testing.T) { 69 | capt := &capture{} 70 | logger := logr.New(newSink(capt.Func, NewFormatterJSON(Options{}))) 71 | slogger := slog.New(logr.ToSlogHandler(logger)) 72 | if len(tc.withAttrs) > 0 { 73 | slogger = slogger.With(tc.withAttrs...) 74 | } 75 | if tc.withGroup != "" { 76 | slogger = slogger.WithGroup(tc.withGroup) 77 | } 78 | slogger.Info("msg", tc.args...) 79 | if capt.log != tc.expect { 80 | t.Errorf("\nexpected %q\n got %q", tc.expect, capt.log) 81 | } 82 | }) 83 | } 84 | } 85 | 86 | func TestSlogSinkGroups(t *testing.T) { 87 | testCases := []struct { 88 | name string 89 | fn func(slogger *slog.Logger) 90 | expect string 91 | }{{ 92 | name: "no group", 93 | fn: func(slogger *slog.Logger) { 94 | slogger. 95 | Info("msg", "k", "v") 96 | }, 97 | expect: `{"logger":"","level":0,"msg":"msg","k":"v"}`, 98 | }, { 99 | name: "1 group with leaf args", 100 | fn: func(slogger *slog.Logger) { 101 | slogger. 102 | WithGroup("g1"). 103 | Info("msg", "k", "v") 104 | }, 105 | expect: `{"logger":"","level":0,"msg":"msg","g1":{"k":"v"}}`, 106 | }, { 107 | name: "1 group without leaf args", 108 | fn: func(slogger *slog.Logger) { 109 | slogger. 110 | WithGroup("g1"). 111 | Info("msg") 112 | }, 113 | expect: `{"logger":"","level":0,"msg":"msg"}`, 114 | }, { 115 | name: "1 group with value without leaf args", 116 | fn: func(slogger *slog.Logger) { 117 | slogger. 118 | WithGroup("g1").With("k1", 1). 119 | Info("msg") 120 | }, 121 | expect: `{"logger":"","level":0,"msg":"msg","g1":{"k1":1}}`, 122 | }, { 123 | name: "2 groups with values no leaf args", 124 | fn: func(slogger *slog.Logger) { 125 | slogger. 126 | WithGroup("g1").With("k1", 1). 127 | WithGroup("g2").With("k2", 2). 128 | Info("msg") 129 | }, 130 | expect: `{"logger":"","level":0,"msg":"msg","g1":{"k1":1,"g2":{"k2":2}}}`, 131 | }, { 132 | name: "3 empty groups with no values or leaf args", 133 | fn: func(slogger *slog.Logger) { 134 | slogger. 135 | WithGroup("g1"). 136 | WithGroup("g2"). 137 | WithGroup("g3"). 138 | Info("msg") 139 | }, 140 | expect: `{"logger":"","level":0,"msg":"msg"}`, 141 | }, { 142 | name: "3 empty groups with no values but with leaf args", 143 | fn: func(slogger *slog.Logger) { 144 | slogger. 145 | WithGroup("g1"). 146 | WithGroup("g2"). 147 | WithGroup("g3"). 148 | Info("msg", "k", "v") 149 | }, 150 | expect: `{"logger":"","level":0,"msg":"msg","g1":{"g2":{"g3":{"k":"v"}}}}`, 151 | }, { 152 | name: "multiple groups with and without values", 153 | fn: func(slogger *slog.Logger) { 154 | slogger. 155 | With("k0", 0). 156 | WithGroup("g1"). 157 | WithGroup("g2"). 158 | WithGroup("g3").With("k3", 3). 159 | WithGroup("g4"). 160 | WithGroup("g5"). 161 | WithGroup("g6").With("k6", 6). 162 | WithGroup("g7"). 163 | WithGroup("g8"). 164 | WithGroup("g9"). 165 | Info("msg") 166 | }, 167 | expect: `{"logger":"","level":0,"msg":"msg","k0":0,"g1":{"g2":{"g3":{"k3":3,"g4":{"g5":{"g6":{"k6":6}}}}}}}`, 168 | }} 169 | 170 | for _, tc := range testCases { 171 | t.Run(tc.name, func(t *testing.T) { 172 | capt := &capture{} 173 | logger := logr.New(newSink(capt.Func, NewFormatterJSON(Options{}))) 174 | slogger := slog.New(logr.ToSlogHandler(logger)) 175 | tc.fn(slogger) 176 | if capt.log != tc.expect { 177 | t.Errorf("\nexpected: `%s`\n got: `%s`", tc.expect, capt.log) 178 | } 179 | }) 180 | } 181 | } 182 | 183 | func TestSlogSinkWithCaller(t *testing.T) { 184 | capt := &capture{} 185 | logger := logr.New(newSink(capt.Func, NewFormatterJSON(Options{LogCaller: All}))) 186 | slogger := slog.New(logr.ToSlogHandler(logger)) 187 | slogger.Error("msg", "int", 1) 188 | _, file, line, _ := runtime.Caller(0) 189 | expect := fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d},"msg":"msg","error":null,"int":1}`, filepath.Base(file), line-1) 190 | if capt.log != expect { 191 | t.Errorf("\nexpected %q\n got %q", expect, capt.log) 192 | } 193 | } 194 | 195 | func TestRunSlogTests(t *testing.T) { 196 | fn := func(buffer *bytes.Buffer) slog.Handler { 197 | printfn := func(obj string) { 198 | fmt.Fprintln(buffer, obj) 199 | } 200 | opts := Options{ 201 | LogTimestamp: true, 202 | Verbosity: 10, 203 | RenderBuiltinsHook: func(kvList []any) []any { 204 | mappedKVList := make([]any, len(kvList)) 205 | for i := 0; i < len(kvList); i += 2 { 206 | key := kvList[i] 207 | switch key { 208 | case "ts": 209 | mappedKVList[i] = "time" 210 | default: 211 | mappedKVList[i] = key 212 | } 213 | mappedKVList[i+1] = kvList[i+1] 214 | } 215 | return mappedKVList 216 | }, 217 | } 218 | logger := NewJSON(printfn, opts) 219 | return logr.ToSlogHandler(logger) 220 | } 221 | exceptions := []string{ 222 | "a Handler should ignore a zero Record.Time", // Time is generated by sink. 223 | } 224 | testhelp.RunSlogTests(t, fn, exceptions...) 225 | } 226 | 227 | func TestLogrSlogConversion(t *testing.T) { 228 | f := New(func(_, _ string) {}, Options{}) 229 | f2 := logr.FromSlogHandler(logr.ToSlogHandler(f)) 230 | if want, got := f, f2; got != want { 231 | t.Helper() 232 | t.Errorf("Expected %T %+v, got instead: %T %+v", want, want, got, got) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-logr/logr 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /internal/testhelp/slog.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Package testhelp holds helper functions for the testing of logr and built-in 21 | // implementations. 22 | package testhelp 23 | 24 | import ( 25 | "bytes" 26 | "encoding/json" 27 | "log/slog" 28 | "strings" 29 | "testing" 30 | "testing/slogtest" 31 | ) 32 | 33 | // RunSlogTests runs slogtest.TestHandler on a given slog.Handler, which is 34 | // expected to emit JSON into the provided buffer. 35 | func RunSlogTests(t *testing.T, createHandler func(buffer *bytes.Buffer) slog.Handler, exceptions ...string) { 36 | var buffer bytes.Buffer 37 | handler := createHandler(&buffer) 38 | err := slogtest.TestHandler(handler, func() []map[string]any { 39 | var ms []map[string]any 40 | for _, line := range bytes.Split(buffer.Bytes(), []byte{'\n'}) { 41 | if len(line) == 0 { 42 | continue 43 | } 44 | var m map[string]any 45 | if err := json.Unmarshal(line, &m); err != nil { 46 | t.Errorf("%v: %q", err, string(line)) 47 | } 48 | ms = append(ms, m) 49 | } 50 | return ms 51 | }) 52 | 53 | // Correlating failures with individual test cases is hard with the current API. 54 | // See https://github.com/golang/go/issues/61758 55 | t.Logf("Output:\n%s", buffer.String()) 56 | if err != nil { 57 | if unwrappable, ok := err.(interface { 58 | Unwrap() []error 59 | }); ok { 60 | for _, err := range unwrappable.Unwrap() { 61 | if !containsOne(err.Error(), exceptions...) { 62 | t.Errorf("Unexpected error: %v", err) 63 | } 64 | } 65 | } else { 66 | // Shouldn't be reached, errors from errors.Join can be split up. 67 | t.Errorf("Unexpected errors:\n%v", err) 68 | } 69 | } 70 | } 71 | 72 | func containsOne(hay string, needles ...string) bool { 73 | for _, needle := range needles { 74 | if strings.Contains(hay, needle) { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | -------------------------------------------------------------------------------- /internal/testhelp/slog_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package testhelp 21 | 22 | import ( 23 | "bytes" 24 | "log/slog" 25 | "testing" 26 | ) 27 | 28 | func TestRunSlogTestsOnSlogSink(t *testing.T) { 29 | // This proves that RunSlogTests works. 30 | RunSlogTests(t, func(buffer *bytes.Buffer) slog.Handler { 31 | return slog.NewJSONHandler(buffer, nil) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /logr.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // This design derives from Dave Cheney's blog: 18 | // http://dave.cheney.net/2015/11/05/lets-talk-about-logging 19 | 20 | // Package logr defines a general-purpose logging API and abstract interfaces 21 | // to back that API. Packages in the Go ecosystem can depend on this package, 22 | // while callers can implement logging with whatever backend is appropriate. 23 | // 24 | // # Usage 25 | // 26 | // Logging is done using a Logger instance. Logger is a concrete type with 27 | // methods, which defers the actual logging to a LogSink interface. The main 28 | // methods of Logger are Info() and Error(). Arguments to Info() and Error() 29 | // are key/value pairs rather than printf-style formatted strings, emphasizing 30 | // "structured logging". 31 | // 32 | // With Go's standard log package, we might write: 33 | // 34 | // log.Printf("setting target value %s", targetValue) 35 | // 36 | // With logr's structured logging, we'd write: 37 | // 38 | // logger.Info("setting target", "value", targetValue) 39 | // 40 | // Errors are much the same. Instead of: 41 | // 42 | // log.Printf("failed to open the pod bay door for user %s: %v", user, err) 43 | // 44 | // We'd write: 45 | // 46 | // logger.Error(err, "failed to open the pod bay door", "user", user) 47 | // 48 | // Info() and Error() are very similar, but they are separate methods so that 49 | // LogSink implementations can choose to do things like attach additional 50 | // information (such as stack traces) on calls to Error(). Error() messages are 51 | // always logged, regardless of the current verbosity. If there is no error 52 | // instance available, passing nil is valid. 53 | // 54 | // # Verbosity 55 | // 56 | // Often we want to log information only when the application in "verbose 57 | // mode". To write log lines that are more verbose, Logger has a V() method. 58 | // The higher the V-level of a log line, the less critical it is considered. 59 | // Log-lines with V-levels that are not enabled (as per the LogSink) will not 60 | // be written. Level V(0) is the default, and logger.V(0).Info() has the same 61 | // meaning as logger.Info(). Negative V-levels have the same meaning as V(0). 62 | // Error messages do not have a verbosity level and are always logged. 63 | // 64 | // Where we might have written: 65 | // 66 | // if flVerbose >= 2 { 67 | // log.Printf("an unusual thing happened") 68 | // } 69 | // 70 | // We can write: 71 | // 72 | // logger.V(2).Info("an unusual thing happened") 73 | // 74 | // # Logger Names 75 | // 76 | // Logger instances can have name strings so that all messages logged through 77 | // that instance have additional context. For example, you might want to add 78 | // a subsystem name: 79 | // 80 | // logger.WithName("compactor").Info("started", "time", time.Now()) 81 | // 82 | // The WithName() method returns a new Logger, which can be passed to 83 | // constructors or other functions for further use. Repeated use of WithName() 84 | // will accumulate name "segments". These name segments will be joined in some 85 | // way by the LogSink implementation. It is strongly recommended that name 86 | // segments contain simple identifiers (letters, digits, and hyphen), and do 87 | // not contain characters that could muddle the log output or confuse the 88 | // joining operation (e.g. whitespace, commas, periods, slashes, brackets, 89 | // quotes, etc). 90 | // 91 | // # Saved Values 92 | // 93 | // Logger instances can store any number of key/value pairs, which will be 94 | // logged alongside all messages logged through that instance. For example, 95 | // you might want to create a Logger instance per managed object: 96 | // 97 | // With the standard log package, we might write: 98 | // 99 | // log.Printf("decided to set field foo to value %q for object %s/%s", 100 | // targetValue, object.Namespace, object.Name) 101 | // 102 | // With logr we'd write: 103 | // 104 | // // Elsewhere: set up the logger to log the object name. 105 | // obj.logger = mainLogger.WithValues( 106 | // "name", obj.name, "namespace", obj.namespace) 107 | // 108 | // // later on... 109 | // obj.logger.Info("setting foo", "value", targetValue) 110 | // 111 | // # Best Practices 112 | // 113 | // Logger has very few hard rules, with the goal that LogSink implementations 114 | // might have a lot of freedom to differentiate. There are, however, some 115 | // things to consider. 116 | // 117 | // The log message consists of a constant message attached to the log line. 118 | // This should generally be a simple description of what's occurring, and should 119 | // never be a format string. Variable information can then be attached using 120 | // named values. 121 | // 122 | // Keys are arbitrary strings, but should generally be constant values. Values 123 | // may be any Go value, but how the value is formatted is determined by the 124 | // LogSink implementation. 125 | // 126 | // Logger instances are meant to be passed around by value. Code that receives 127 | // such a value can call its methods without having to check whether the 128 | // instance is ready for use. 129 | // 130 | // The zero logger (= Logger{}) is identical to Discard() and discards all log 131 | // entries. Code that receives a Logger by value can simply call it, the methods 132 | // will never crash. For cases where passing a logger is optional, a pointer to Logger 133 | // should be used. 134 | // 135 | // # Key Naming Conventions 136 | // 137 | // Keys are not strictly required to conform to any specification or regex, but 138 | // it is recommended that they: 139 | // - be human-readable and meaningful (not auto-generated or simple ordinals) 140 | // - be constant (not dependent on input data) 141 | // - contain only printable characters 142 | // - not contain whitespace or punctuation 143 | // - use lower case for simple keys and lowerCamelCase for more complex ones 144 | // 145 | // These guidelines help ensure that log data is processed properly regardless 146 | // of the log implementation. For example, log implementations will try to 147 | // output JSON data or will store data for later database (e.g. SQL) queries. 148 | // 149 | // While users are generally free to use key names of their choice, it's 150 | // generally best to avoid using the following keys, as they're frequently used 151 | // by implementations: 152 | // - "caller": the calling information (file/line) of a particular log line 153 | // - "error": the underlying error value in the `Error` method 154 | // - "level": the log level 155 | // - "logger": the name of the associated logger 156 | // - "msg": the log message 157 | // - "stacktrace": the stack trace associated with a particular log line or 158 | // error (often from the `Error` message) 159 | // - "ts": the timestamp for a log line 160 | // 161 | // Implementations are encouraged to make use of these keys to represent the 162 | // above concepts, when necessary (for example, in a pure-JSON output form, it 163 | // would be necessary to represent at least message and timestamp as ordinary 164 | // named values). 165 | // 166 | // # Break Glass 167 | // 168 | // Implementations may choose to give callers access to the underlying 169 | // logging implementation. The recommended pattern for this is: 170 | // 171 | // // Underlier exposes access to the underlying logging implementation. 172 | // // Since callers only have a logr.Logger, they have to know which 173 | // // implementation is in use, so this interface is less of an abstraction 174 | // // and more of way to test type conversion. 175 | // type Underlier interface { 176 | // GetUnderlying() 177 | // } 178 | // 179 | // Logger grants access to the sink to enable type assertions like this: 180 | // 181 | // func DoSomethingWithImpl(log logr.Logger) { 182 | // if underlier, ok := log.GetSink().(impl.Underlier); ok { 183 | // implLogger := underlier.GetUnderlying() 184 | // ... 185 | // } 186 | // } 187 | // 188 | // Custom `With*` functions can be implemented by copying the complete 189 | // Logger struct and replacing the sink in the copy: 190 | // 191 | // // WithFooBar changes the foobar parameter in the log sink and returns a 192 | // // new logger with that modified sink. It does nothing for loggers where 193 | // // the sink doesn't support that parameter. 194 | // func WithFoobar(log logr.Logger, foobar int) logr.Logger { 195 | // if foobarLogSink, ok := log.GetSink().(FoobarSink); ok { 196 | // log = log.WithSink(foobarLogSink.WithFooBar(foobar)) 197 | // } 198 | // return log 199 | // } 200 | // 201 | // Don't use New to construct a new Logger with a LogSink retrieved from an 202 | // existing Logger. Source code attribution might not work correctly and 203 | // unexported fields in Logger get lost. 204 | // 205 | // Beware that the same LogSink instance may be shared by different logger 206 | // instances. Calling functions that modify the LogSink will affect all of 207 | // those. 208 | package logr 209 | 210 | // New returns a new Logger instance. This is primarily used by libraries 211 | // implementing LogSink, rather than end users. Passing a nil sink will create 212 | // a Logger which discards all log lines. 213 | func New(sink LogSink) Logger { 214 | logger := Logger{} 215 | logger.setSink(sink) 216 | if sink != nil { 217 | sink.Init(runtimeInfo) 218 | } 219 | return logger 220 | } 221 | 222 | // setSink stores the sink and updates any related fields. It mutates the 223 | // logger and thus is only safe to use for loggers that are not currently being 224 | // used concurrently. 225 | func (l *Logger) setSink(sink LogSink) { 226 | l.sink = sink 227 | } 228 | 229 | // GetSink returns the stored sink. 230 | func (l Logger) GetSink() LogSink { 231 | return l.sink 232 | } 233 | 234 | // WithSink returns a copy of the logger with the new sink. 235 | func (l Logger) WithSink(sink LogSink) Logger { 236 | l.setSink(sink) 237 | return l 238 | } 239 | 240 | // Logger is an interface to an abstract logging implementation. This is a 241 | // concrete type for performance reasons, but all the real work is passed on to 242 | // a LogSink. Implementations of LogSink should provide their own constructors 243 | // that return Logger, not LogSink. 244 | // 245 | // The underlying sink can be accessed through GetSink and be modified through 246 | // WithSink. This enables the implementation of custom extensions (see "Break 247 | // Glass" in the package documentation). Normally the sink should be used only 248 | // indirectly. 249 | type Logger struct { 250 | sink LogSink 251 | level int 252 | } 253 | 254 | // Enabled tests whether this Logger is enabled. For example, commandline 255 | // flags might be used to set the logging verbosity and disable some info logs. 256 | func (l Logger) Enabled() bool { 257 | // Some implementations of LogSink look at the caller in Enabled (e.g. 258 | // different verbosity levels per package or file), but we only pass one 259 | // CallDepth in (via Init). This means that all calls from Logger to the 260 | // LogSink's Enabled, Info, and Error methods must have the same number of 261 | // frames. In other words, Logger methods can't call other Logger methods 262 | // which call these LogSink methods unless we do it the same in all paths. 263 | return l.sink != nil && l.sink.Enabled(l.level) 264 | } 265 | 266 | // Info logs a non-error message with the given key/value pairs as context. 267 | // 268 | // The msg argument should be used to add some constant description to the log 269 | // line. The key/value pairs can then be used to add additional variable 270 | // information. The key/value pairs must alternate string keys and arbitrary 271 | // values. 272 | func (l Logger) Info(msg string, keysAndValues ...any) { 273 | if l.sink == nil { 274 | return 275 | } 276 | if l.sink.Enabled(l.level) { // see comment in Enabled 277 | if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { 278 | withHelper.GetCallStackHelper()() 279 | } 280 | l.sink.Info(l.level, msg, keysAndValues...) 281 | } 282 | } 283 | 284 | // Error logs an error, with the given message and key/value pairs as context. 285 | // It functions similarly to Info, but may have unique behavior, and should be 286 | // preferred for logging errors (see the package documentations for more 287 | // information). The log message will always be emitted, regardless of 288 | // verbosity level. 289 | // 290 | // The msg argument should be used to add context to any underlying error, 291 | // while the err argument should be used to attach the actual error that 292 | // triggered this log line, if present. The err parameter is optional 293 | // and nil may be passed instead of an error instance. 294 | func (l Logger) Error(err error, msg string, keysAndValues ...any) { 295 | if l.sink == nil { 296 | return 297 | } 298 | if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { 299 | withHelper.GetCallStackHelper()() 300 | } 301 | l.sink.Error(err, msg, keysAndValues...) 302 | } 303 | 304 | // V returns a new Logger instance for a specific verbosity level, relative to 305 | // this Logger. In other words, V-levels are additive. A higher verbosity 306 | // level means a log message is less important. Negative V-levels are treated 307 | // as 0. 308 | func (l Logger) V(level int) Logger { 309 | if l.sink == nil { 310 | return l 311 | } 312 | if level < 0 { 313 | level = 0 314 | } 315 | l.level += level 316 | return l 317 | } 318 | 319 | // GetV returns the verbosity level of the logger. If the logger's LogSink is 320 | // nil as in the Discard logger, this will always return 0. 321 | func (l Logger) GetV() int { 322 | // 0 if l.sink nil because of the if check in V above. 323 | return l.level 324 | } 325 | 326 | // WithValues returns a new Logger instance with additional key/value pairs. 327 | // See Info for documentation on how key/value pairs work. 328 | func (l Logger) WithValues(keysAndValues ...any) Logger { 329 | if l.sink == nil { 330 | return l 331 | } 332 | l.setSink(l.sink.WithValues(keysAndValues...)) 333 | return l 334 | } 335 | 336 | // WithName returns a new Logger instance with the specified name element added 337 | // to the Logger's name. Successive calls with WithName append additional 338 | // suffixes to the Logger's name. It's strongly recommended that name segments 339 | // contain only letters, digits, and hyphens (see the package documentation for 340 | // more information). 341 | func (l Logger) WithName(name string) Logger { 342 | if l.sink == nil { 343 | return l 344 | } 345 | l.setSink(l.sink.WithName(name)) 346 | return l 347 | } 348 | 349 | // WithCallDepth returns a Logger instance that offsets the call stack by the 350 | // specified number of frames when logging call site information, if possible. 351 | // This is useful for users who have helper functions between the "real" call 352 | // site and the actual calls to Logger methods. If depth is 0 the attribution 353 | // should be to the direct caller of this function. If depth is 1 the 354 | // attribution should skip 1 call frame, and so on. Successive calls to this 355 | // are additive. 356 | // 357 | // If the underlying log implementation supports a WithCallDepth(int) method, 358 | // it will be called and the result returned. If the implementation does not 359 | // support CallDepthLogSink, the original Logger will be returned. 360 | // 361 | // To skip one level, WithCallStackHelper() should be used instead of 362 | // WithCallDepth(1) because it works with implementions that support the 363 | // CallDepthLogSink and/or CallStackHelperLogSink interfaces. 364 | func (l Logger) WithCallDepth(depth int) Logger { 365 | if l.sink == nil { 366 | return l 367 | } 368 | if withCallDepth, ok := l.sink.(CallDepthLogSink); ok { 369 | l.setSink(withCallDepth.WithCallDepth(depth)) 370 | } 371 | return l 372 | } 373 | 374 | // WithCallStackHelper returns a new Logger instance that skips the direct 375 | // caller when logging call site information, if possible. This is useful for 376 | // users who have helper functions between the "real" call site and the actual 377 | // calls to Logger methods and want to support loggers which depend on marking 378 | // each individual helper function, like loggers based on testing.T. 379 | // 380 | // In addition to using that new logger instance, callers also must call the 381 | // returned function. 382 | // 383 | // If the underlying log implementation supports a WithCallDepth(int) method, 384 | // WithCallDepth(1) will be called to produce a new logger. If it supports a 385 | // WithCallStackHelper() method, that will be also called. If the 386 | // implementation does not support either of these, the original Logger will be 387 | // returned. 388 | func (l Logger) WithCallStackHelper() (func(), Logger) { 389 | if l.sink == nil { 390 | return func() {}, l 391 | } 392 | var helper func() 393 | if withCallDepth, ok := l.sink.(CallDepthLogSink); ok { 394 | l.setSink(withCallDepth.WithCallDepth(1)) 395 | } 396 | if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { 397 | helper = withHelper.GetCallStackHelper() 398 | } else { 399 | helper = func() {} 400 | } 401 | return helper, l 402 | } 403 | 404 | // IsZero returns true if this logger is an uninitialized zero value 405 | func (l Logger) IsZero() bool { 406 | return l.sink == nil 407 | } 408 | 409 | // RuntimeInfo holds information that the logr "core" library knows which 410 | // LogSinks might want to know. 411 | type RuntimeInfo struct { 412 | // CallDepth is the number of call frames the logr library adds between the 413 | // end-user and the LogSink. LogSink implementations which choose to print 414 | // the original logging site (e.g. file & line) should climb this many 415 | // additional frames to find it. 416 | CallDepth int 417 | } 418 | 419 | // runtimeInfo is a static global. It must not be changed at run time. 420 | var runtimeInfo = RuntimeInfo{ 421 | CallDepth: 1, 422 | } 423 | 424 | // LogSink represents a logging implementation. End-users will generally not 425 | // interact with this type. 426 | type LogSink interface { 427 | // Init receives optional information about the logr library for LogSink 428 | // implementations that need it. 429 | Init(info RuntimeInfo) 430 | 431 | // Enabled tests whether this LogSink is enabled at the specified V-level. 432 | // For example, commandline flags might be used to set the logging 433 | // verbosity and disable some info logs. 434 | Enabled(level int) bool 435 | 436 | // Info logs a non-error message with the given key/value pairs as context. 437 | // The level argument is provided for optional logging. This method will 438 | // only be called when Enabled(level) is true. See Logger.Info for more 439 | // details. 440 | Info(level int, msg string, keysAndValues ...any) 441 | 442 | // Error logs an error, with the given message and key/value pairs as 443 | // context. See Logger.Error for more details. 444 | Error(err error, msg string, keysAndValues ...any) 445 | 446 | // WithValues returns a new LogSink with additional key/value pairs. See 447 | // Logger.WithValues for more details. 448 | WithValues(keysAndValues ...any) LogSink 449 | 450 | // WithName returns a new LogSink with the specified name appended. See 451 | // Logger.WithName for more details. 452 | WithName(name string) LogSink 453 | } 454 | 455 | // CallDepthLogSink represents a LogSink that knows how to climb the call stack 456 | // to identify the original call site and can offset the depth by a specified 457 | // number of frames. This is useful for users who have helper functions 458 | // between the "real" call site and the actual calls to Logger methods. 459 | // Implementations that log information about the call site (such as file, 460 | // function, or line) would otherwise log information about the intermediate 461 | // helper functions. 462 | // 463 | // This is an optional interface and implementations are not required to 464 | // support it. 465 | type CallDepthLogSink interface { 466 | // WithCallDepth returns a LogSink that will offset the call 467 | // stack by the specified number of frames when logging call 468 | // site information. 469 | // 470 | // If depth is 0, the LogSink should skip exactly the number 471 | // of call frames defined in RuntimeInfo.CallDepth when Info 472 | // or Error are called, i.e. the attribution should be to the 473 | // direct caller of Logger.Info or Logger.Error. 474 | // 475 | // If depth is 1 the attribution should skip 1 call frame, and so on. 476 | // Successive calls to this are additive. 477 | WithCallDepth(depth int) LogSink 478 | } 479 | 480 | // CallStackHelperLogSink represents a LogSink that knows how to climb 481 | // the call stack to identify the original call site and can skip 482 | // intermediate helper functions if they mark themselves as 483 | // helper. Go's testing package uses that approach. 484 | // 485 | // This is useful for users who have helper functions between the 486 | // "real" call site and the actual calls to Logger methods. 487 | // Implementations that log information about the call site (such as 488 | // file, function, or line) would otherwise log information about the 489 | // intermediate helper functions. 490 | // 491 | // This is an optional interface and implementations are not required 492 | // to support it. Implementations that choose to support this must not 493 | // simply implement it as WithCallDepth(1), because 494 | // Logger.WithCallStackHelper will call both methods if they are 495 | // present. This should only be implemented for LogSinks that actually 496 | // need it, as with testing.T. 497 | type CallStackHelperLogSink interface { 498 | // GetCallStackHelper returns a function that must be called 499 | // to mark the direct caller as helper function when logging 500 | // call site information. 501 | GetCallStackHelper() func() 502 | } 503 | 504 | // Marshaler is an optional interface that logged values may choose to 505 | // implement. Loggers with structured output, such as JSON, should 506 | // log the object return by the MarshalLog method instead of the 507 | // original value. 508 | type Marshaler interface { 509 | // MarshalLog can be used to: 510 | // - ensure that structs are not logged as strings when the original 511 | // value has a String method: return a different type without a 512 | // String method 513 | // - select which fields of a complex type should get logged: 514 | // return a simpler struct with fewer fields 515 | // - log unexported fields: return a different struct 516 | // with exported fields 517 | // 518 | // It may return any value of any type. 519 | MarshalLog() any 520 | } 521 | -------------------------------------------------------------------------------- /logr_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logr 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "reflect" 23 | "runtime" 24 | "testing" 25 | ) 26 | 27 | func TestNew(t *testing.T) { 28 | calledInit := 0 29 | 30 | sink := &testLogSink{} 31 | sink.fnInit = func(ri RuntimeInfo) { 32 | if ri.CallDepth != 1 { 33 | t.Errorf("expected runtimeInfo.CallDepth = 1, got %d", ri.CallDepth) 34 | } 35 | calledInit++ 36 | } 37 | logger := New(sink) 38 | 39 | if logger.sink == nil { 40 | t.Errorf("expected sink to be set, got %v", logger.sink) 41 | } 42 | if calledInit != 1 { 43 | t.Errorf("expected sink.Init() to be called once, got %d", calledInit) 44 | } 45 | if _, ok := logger.sink.(CallDepthLogSink); ok { 46 | t.Errorf("expected conversion to CallDepthLogSink to fail") 47 | } 48 | } 49 | 50 | func TestNewCachesCallDepthInterface(t *testing.T) { 51 | sink := &testCallDepthLogSink{} 52 | logger := New(sink) 53 | 54 | if _, ok := logger.sink.(CallDepthLogSink); !ok { 55 | t.Errorf("expected conversion to CallDepthLogSink to succeed") 56 | } 57 | } 58 | 59 | func TestEnabled(t *testing.T) { 60 | calledEnabled := 0 61 | 62 | sink := &testLogSink{} 63 | sink.fnEnabled = func(_ int) bool { 64 | calledEnabled++ 65 | return true 66 | } 67 | logger := New(sink) 68 | 69 | if en := logger.Enabled(); en != true { 70 | t.Errorf("expected true") 71 | } 72 | if calledEnabled != 1 { 73 | t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) 74 | } 75 | } 76 | 77 | func TestError(t *testing.T) { 78 | calledError := 0 79 | errInput := fmt.Errorf("error") 80 | msgInput := "msg" 81 | kvInput := []any{0, 1, 2} 82 | 83 | sink := &testLogSink{} 84 | sink.fnError = func(err error, msg string, kv ...any) { 85 | calledError++ 86 | if err != errInput { 87 | t.Errorf("unexpected err input, got %v", err) 88 | } 89 | if msg != msgInput { 90 | t.Errorf("unexpected msg input, got %q", msg) 91 | } 92 | if !reflect.DeepEqual(kv, kvInput) { 93 | t.Errorf("unexpected kv input, got %v", kv) 94 | } 95 | } 96 | logger := New(sink) 97 | 98 | logger.Error(errInput, msgInput, kvInput...) 99 | if calledError != 1 { 100 | t.Errorf("expected sink.Error() to be called once, got %d", calledError) 101 | } 102 | } 103 | 104 | func TestV(t *testing.T) { 105 | for name, logger := range map[string]Logger{ 106 | "testLogSink": New(&testLogSink{}), 107 | "Discard": Discard(), 108 | "Zero": {}, 109 | } { 110 | t.Run(name, func(t *testing.T) { 111 | adjust := func(level int) int { 112 | if logger.GetSink() == nil { 113 | // The Discard and the zero Logger short-cut the V call and don't 114 | // change the verbosity level. 115 | return 0 116 | } 117 | return level 118 | } 119 | inputs := []struct { 120 | name string 121 | fn func() Logger 122 | exp int 123 | }{{ 124 | name: "V(0)", 125 | fn: func() Logger { return logger.V(0) }, 126 | exp: 0, 127 | }, { 128 | name: "V(93)", 129 | fn: func() Logger { return logger.V(93) }, 130 | exp: adjust(93), 131 | }, { 132 | name: "V(70).V(6)", 133 | fn: func() Logger { return logger.V(70).V(6) }, 134 | exp: adjust(76), 135 | }, { 136 | name: "V(-1)", 137 | fn: func() Logger { return logger.V(-1) }, 138 | exp: 0, 139 | }, { 140 | name: "V(1).V(-1)", 141 | fn: func() Logger { return logger.V(1).V(-1) }, 142 | exp: adjust(1), 143 | }} 144 | for _, in := range inputs { 145 | t.Run(in.name, func(t *testing.T) { 146 | if want, got := in.exp, in.fn().GetV(); got != want { 147 | t.Errorf("expected %d, got %d", want, got) 148 | } 149 | }) 150 | } 151 | }) 152 | } 153 | } 154 | 155 | func TestInfo(t *testing.T) { 156 | calledEnabled := 0 157 | calledInfo := 0 158 | lvlInput := 0 159 | msgInput := "msg" 160 | kvInput := []any{0, 1, 2} 161 | 162 | sink := &testLogSink{} 163 | sink.fnEnabled = func(lvl int) bool { 164 | calledEnabled++ 165 | return lvl < 100 166 | } 167 | sink.fnInfo = func(lvl int, msg string, kv ...any) { 168 | calledInfo++ 169 | if lvl != lvlInput { 170 | t.Errorf("unexpected lvl input, got %v", lvl) 171 | } 172 | if msg != msgInput { 173 | t.Errorf("unexpected msg input, got %q", msg) 174 | } 175 | if !reflect.DeepEqual(kv, kvInput) { 176 | t.Errorf("unexpected kv input, got %v", kv) 177 | } 178 | } 179 | logger := New(sink) 180 | 181 | calledEnabled = 0 182 | calledInfo = 0 183 | lvlInput = 0 184 | logger.Info(msgInput, kvInput...) 185 | if calledEnabled != 1 { 186 | t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) 187 | } 188 | if calledInfo != 1 { 189 | t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) 190 | } 191 | 192 | calledEnabled = 0 193 | calledInfo = 0 194 | lvlInput = 0 195 | logger.V(0).Info(msgInput, kvInput...) 196 | if calledEnabled != 1 { 197 | t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) 198 | } 199 | if calledInfo != 1 { 200 | t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) 201 | } 202 | 203 | calledEnabled = 0 204 | calledInfo = 0 205 | lvlInput = 93 206 | logger.V(93).Info(msgInput, kvInput...) 207 | if calledEnabled != 1 { 208 | t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) 209 | } 210 | if calledInfo != 1 { 211 | t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) 212 | } 213 | 214 | calledEnabled = 0 215 | calledInfo = 0 216 | lvlInput = 100 217 | logger.V(100).Info(msgInput, kvInput...) 218 | if calledEnabled != 1 { 219 | t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) 220 | } 221 | if calledInfo != 0 { 222 | t.Errorf("expected sink.Info() to not be called, got %d", calledInfo) 223 | } 224 | } 225 | 226 | func TestWithValues(t *testing.T) { 227 | calledWithValues := 0 228 | kvInput := []any{"zero", 0, "one", 1, "two", 2} 229 | 230 | sink := &testLogSink{} 231 | sink.fnWithValues = func(kv ...any) { 232 | calledWithValues++ 233 | if !reflect.DeepEqual(kv, kvInput) { 234 | t.Errorf("unexpected kv input, got %v", kv) 235 | } 236 | } 237 | logger := New(sink) 238 | 239 | out := logger.WithValues(kvInput...) 240 | if calledWithValues != 1 { 241 | t.Errorf("expected sink.WithValues() to be called once, got %d", calledWithValues) 242 | } 243 | if p, _ := out.sink.(*testLogSink); p == sink { 244 | t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) 245 | } 246 | } 247 | 248 | func TestWithName(t *testing.T) { 249 | calledWithName := 0 250 | nameInput := "name" 251 | 252 | sink := &testLogSink{} 253 | sink.fnWithName = func(name string) { 254 | calledWithName++ 255 | if name != nameInput { 256 | t.Errorf("unexpected name input, got %q", name) 257 | } 258 | } 259 | logger := New(sink) 260 | 261 | out := logger.WithName(nameInput) 262 | if calledWithName != 1 { 263 | t.Errorf("expected sink.WithName() to be called once, got %d", calledWithName) 264 | } 265 | if p, _ := out.sink.(*testLogSink); p == sink { 266 | t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) 267 | } 268 | } 269 | 270 | func TestWithCallDepthNotImplemented(t *testing.T) { 271 | depthInput := 7 272 | 273 | sink := &testLogSink{} 274 | logger := New(sink) 275 | 276 | out := logger.WithCallDepth(depthInput) 277 | if p, _ := out.sink.(*testLogSink); p != sink { 278 | t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) 279 | } 280 | } 281 | 282 | func TestWithCallDepthImplemented(t *testing.T) { 283 | calledWithCallDepth := 0 284 | depthInput := 7 285 | 286 | sink := &testCallDepthLogSink{} 287 | sink.fnWithCallDepth = func(depth int) { 288 | calledWithCallDepth++ 289 | if depth != depthInput { 290 | t.Errorf("unexpected depth input, got %d", depth) 291 | } 292 | } 293 | logger := New(sink) 294 | 295 | out := logger.WithCallDepth(depthInput) 296 | if calledWithCallDepth != 1 { 297 | t.Errorf("expected sink.WithCallDepth() to be called once, got %d", calledWithCallDepth) 298 | } 299 | p, _ := out.sink.(*testCallDepthLogSink) 300 | if p == sink { 301 | t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) 302 | } 303 | if p.callDepth != depthInput { 304 | t.Errorf("expected sink to have call depth %d, got %d", depthInput, p.callDepth) 305 | } 306 | } 307 | 308 | func TestWithCallDepthIncremental(t *testing.T) { 309 | calledWithCallDepth := 0 310 | depthInput := 7 311 | 312 | sink := &testCallDepthLogSink{} 313 | sink.fnWithCallDepth = func(depth int) { 314 | calledWithCallDepth++ 315 | if depth != 1 { 316 | t.Errorf("unexpected depth input, got %d", depth) 317 | } 318 | } 319 | logger := New(sink) 320 | 321 | out := logger 322 | for i := 0; i < depthInput; i++ { 323 | out = out.WithCallDepth(1) 324 | } 325 | if calledWithCallDepth != depthInput { 326 | t.Errorf("expected sink.WithCallDepth() to be called %d times, got %d", depthInput, calledWithCallDepth) 327 | } 328 | p, _ := out.sink.(*testCallDepthLogSink) 329 | if p == sink { 330 | t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) 331 | } 332 | if p.callDepth != depthInput { 333 | t.Errorf("expected sink to have call depth %d, got %d", depthInput, p.callDepth) 334 | } 335 | } 336 | 337 | func TestIsZero(t *testing.T) { 338 | var l Logger 339 | if !l.IsZero() { 340 | t.Errorf("expected IsZero") 341 | } 342 | sink := &testLogSink{} 343 | l = New(sink) 344 | if l.IsZero() { 345 | t.Errorf("expected not IsZero") 346 | } 347 | // Discard is the same as a nil sink. 348 | l = Discard() 349 | if !l.IsZero() { 350 | t.Errorf("expected IsZero") 351 | } 352 | } 353 | 354 | func TestZeroValue(t *testing.T) { 355 | // Make sure that the zero value is useful and equivalent to a Discard logger. 356 | var l Logger 357 | if l.Enabled() { 358 | t.Errorf("expected not Enabled") 359 | } 360 | if !l.IsZero() { 361 | t.Errorf("expected IsZero") 362 | } 363 | // Make sure that none of these methods cause a crash 364 | l.Info("foo") 365 | l.Error(errors.New("bar"), "some error") 366 | if l.GetSink() != nil { 367 | t.Errorf("expected nil from GetSink") 368 | } 369 | l2 := l.WithName("some-name").V(2).WithValues("foo", 1).WithCallDepth(1) 370 | l2.Info("foo") 371 | l2.Error(errors.New("bar"), "some error") 372 | _, _ = l.WithCallStackHelper() 373 | } 374 | 375 | func TestCallDepthConsistent(t *testing.T) { 376 | sink := &testLogSink{} 377 | 378 | depth := 0 379 | expect := "github.com/go-logr/logr.TestCallDepthConsistent" 380 | sink.fnInit = func(ri RuntimeInfo) { 381 | depth = ri.CallDepth + 1 // 1 for these function pointers 382 | if caller := getCaller(depth); caller != expect { 383 | t.Errorf("identified wrong caller %q", caller) 384 | } 385 | 386 | } 387 | sink.fnEnabled = func(_ int) bool { 388 | if caller := getCaller(depth); caller != expect { 389 | t.Errorf("identified wrong caller %q", caller) 390 | } 391 | return true 392 | } 393 | sink.fnError = func(_ error, _ string, _ ...any) { 394 | if caller := getCaller(depth); caller != expect { 395 | t.Errorf("identified wrong caller %q", caller) 396 | } 397 | } 398 | l := New(sink) 399 | 400 | l.Enabled() 401 | l.Info("msg") 402 | l.Error(nil, "msg") 403 | } 404 | 405 | func getCaller(depth int) string { 406 | // +1 for this frame, +1 for Info/Error/Enabled. 407 | pc, _, _, ok := runtime.Caller(depth + 2) 408 | if !ok { 409 | return "" 410 | } 411 | fp := runtime.FuncForPC(pc) 412 | if fp == nil { 413 | return "" 414 | } 415 | return fp.Name() 416 | } 417 | -------------------------------------------------------------------------------- /sloghandler.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package logr 21 | 22 | import ( 23 | "context" 24 | "log/slog" 25 | ) 26 | 27 | type slogHandler struct { 28 | // May be nil, in which case all logs get discarded. 29 | sink LogSink 30 | // Non-nil if sink is non-nil and implements SlogSink. 31 | slogSink SlogSink 32 | 33 | // groupPrefix collects values from WithGroup calls. It gets added as 34 | // prefix to value keys when handling a log record. 35 | groupPrefix string 36 | 37 | // levelBias can be set when constructing the handler to influence the 38 | // slog.Level of log records. A positive levelBias reduces the 39 | // slog.Level value. slog has no API to influence this value after the 40 | // handler got created, so it can only be set indirectly through 41 | // Logger.V. 42 | levelBias slog.Level 43 | } 44 | 45 | var _ slog.Handler = &slogHandler{} 46 | 47 | // groupSeparator is used to concatenate WithGroup names and attribute keys. 48 | const groupSeparator = "." 49 | 50 | // GetLevel is used for black box unit testing. 51 | func (l *slogHandler) GetLevel() slog.Level { 52 | return l.levelBias 53 | } 54 | 55 | func (l *slogHandler) Enabled(_ context.Context, level slog.Level) bool { 56 | return l.sink != nil && (level >= slog.LevelError || l.sink.Enabled(l.levelFromSlog(level))) 57 | } 58 | 59 | func (l *slogHandler) Handle(ctx context.Context, record slog.Record) error { 60 | if l.slogSink != nil { 61 | // Only adjust verbosity level of log entries < slog.LevelError. 62 | if record.Level < slog.LevelError { 63 | record.Level -= l.levelBias 64 | } 65 | return l.slogSink.Handle(ctx, record) 66 | } 67 | 68 | // No need to check for nil sink here because Handle will only be called 69 | // when Enabled returned true. 70 | 71 | kvList := make([]any, 0, 2*record.NumAttrs()) 72 | record.Attrs(func(attr slog.Attr) bool { 73 | kvList = attrToKVs(attr, l.groupPrefix, kvList) 74 | return true 75 | }) 76 | if record.Level >= slog.LevelError { 77 | l.sinkWithCallDepth().Error(nil, record.Message, kvList...) 78 | } else { 79 | level := l.levelFromSlog(record.Level) 80 | l.sinkWithCallDepth().Info(level, record.Message, kvList...) 81 | } 82 | return nil 83 | } 84 | 85 | // sinkWithCallDepth adjusts the stack unwinding so that when Error or Info 86 | // are called by Handle, code in slog gets skipped. 87 | // 88 | // This offset currently (Go 1.21.0) works for calls through 89 | // slog.New(ToSlogHandler(...)). There's no guarantee that the call 90 | // chain won't change. Wrapping the handler will also break unwinding. It's 91 | // still better than not adjusting at all.... 92 | // 93 | // This cannot be done when constructing the handler because FromSlogHandler needs 94 | // access to the original sink without this adjustment. A second copy would 95 | // work, but then WithAttrs would have to be called for both of them. 96 | func (l *slogHandler) sinkWithCallDepth() LogSink { 97 | if sink, ok := l.sink.(CallDepthLogSink); ok { 98 | return sink.WithCallDepth(2) 99 | } 100 | return l.sink 101 | } 102 | 103 | func (l *slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { 104 | if l.sink == nil || len(attrs) == 0 { 105 | return l 106 | } 107 | 108 | clone := *l 109 | if l.slogSink != nil { 110 | clone.slogSink = l.slogSink.WithAttrs(attrs) 111 | clone.sink = clone.slogSink 112 | } else { 113 | kvList := make([]any, 0, 2*len(attrs)) 114 | for _, attr := range attrs { 115 | kvList = attrToKVs(attr, l.groupPrefix, kvList) 116 | } 117 | clone.sink = l.sink.WithValues(kvList...) 118 | } 119 | return &clone 120 | } 121 | 122 | func (l *slogHandler) WithGroup(name string) slog.Handler { 123 | if l.sink == nil { 124 | return l 125 | } 126 | if name == "" { 127 | // slog says to inline empty groups 128 | return l 129 | } 130 | clone := *l 131 | if l.slogSink != nil { 132 | clone.slogSink = l.slogSink.WithGroup(name) 133 | clone.sink = clone.slogSink 134 | } else { 135 | clone.groupPrefix = addPrefix(clone.groupPrefix, name) 136 | } 137 | return &clone 138 | } 139 | 140 | // attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups 141 | // and other details of slog. 142 | func attrToKVs(attr slog.Attr, groupPrefix string, kvList []any) []any { 143 | attrVal := attr.Value.Resolve() 144 | if attrVal.Kind() == slog.KindGroup { 145 | groupVal := attrVal.Group() 146 | grpKVs := make([]any, 0, 2*len(groupVal)) 147 | prefix := groupPrefix 148 | if attr.Key != "" { 149 | prefix = addPrefix(groupPrefix, attr.Key) 150 | } 151 | for _, attr := range groupVal { 152 | grpKVs = attrToKVs(attr, prefix, grpKVs) 153 | } 154 | kvList = append(kvList, grpKVs...) 155 | } else if attr.Key != "" { 156 | kvList = append(kvList, addPrefix(groupPrefix, attr.Key), attrVal.Any()) 157 | } 158 | 159 | return kvList 160 | } 161 | 162 | func addPrefix(prefix, name string) string { 163 | if prefix == "" { 164 | return name 165 | } 166 | if name == "" { 167 | return prefix 168 | } 169 | return prefix + groupSeparator + name 170 | } 171 | 172 | // levelFromSlog adjusts the level by the logger's verbosity and negates it. 173 | // It ensures that the result is >= 0. This is necessary because the result is 174 | // passed to a LogSink and that API did not historically document whether 175 | // levels could be negative or what that meant. 176 | // 177 | // Some example usage: 178 | // 179 | // logrV0 := getMyLogger() 180 | // logrV2 := logrV0.V(2) 181 | // slogV2 := slog.New(logr.ToSlogHandler(logrV2)) 182 | // slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6) 183 | // slogV2.Info("msg") // =~ logrV2.V(0) =~ logrV0.V(2) 184 | // slogv2.Warn("msg") // =~ logrV2.V(-4) =~ logrV0.V(0) 185 | func (l *slogHandler) levelFromSlog(level slog.Level) int { 186 | result := -level 187 | result += l.levelBias // in case the original Logger had a V level 188 | if result < 0 { 189 | result = 0 // because LogSink doesn't expect negative V levels 190 | } 191 | return int(result) 192 | } 193 | -------------------------------------------------------------------------------- /slogr.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package logr 21 | 22 | import ( 23 | "context" 24 | "log/slog" 25 | ) 26 | 27 | // FromSlogHandler returns a Logger which writes to the slog.Handler. 28 | // 29 | // The logr verbosity level is mapped to slog levels such that V(0) becomes 30 | // slog.LevelInfo and V(4) becomes slog.LevelDebug. 31 | func FromSlogHandler(handler slog.Handler) Logger { 32 | if handler, ok := handler.(*slogHandler); ok { 33 | if handler.sink == nil { 34 | return Discard() 35 | } 36 | return New(handler.sink).V(int(handler.levelBias)) 37 | } 38 | return New(&slogSink{handler: handler}) 39 | } 40 | 41 | // ToSlogHandler returns a slog.Handler which writes to the same sink as the Logger. 42 | // 43 | // The returned logger writes all records with level >= slog.LevelError as 44 | // error log entries with LogSink.Error, regardless of the verbosity level of 45 | // the Logger: 46 | // 47 | // logger := 48 | // slog.New(ToSlogHandler(logger.V(10))).Error(...) -> logSink.Error(...) 49 | // 50 | // The level of all other records gets reduced by the verbosity 51 | // level of the Logger and the result is negated. If it happens 52 | // to be negative, then it gets replaced by zero because a LogSink 53 | // is not expected to handled negative levels: 54 | // 55 | // slog.New(ToSlogHandler(logger)).Debug(...) -> logger.GetSink().Info(level=4, ...) 56 | // slog.New(ToSlogHandler(logger)).Warning(...) -> logger.GetSink().Info(level=0, ...) 57 | // slog.New(ToSlogHandler(logger)).Info(...) -> logger.GetSink().Info(level=0, ...) 58 | // slog.New(ToSlogHandler(logger.V(4))).Info(...) -> logger.GetSink().Info(level=4, ...) 59 | func ToSlogHandler(logger Logger) slog.Handler { 60 | if sink, ok := logger.GetSink().(*slogSink); ok && logger.GetV() == 0 { 61 | return sink.handler 62 | } 63 | 64 | handler := &slogHandler{sink: logger.GetSink(), levelBias: slog.Level(logger.GetV())} 65 | if slogSink, ok := handler.sink.(SlogSink); ok { 66 | handler.slogSink = slogSink 67 | } 68 | return handler 69 | } 70 | 71 | // SlogSink is an optional interface that a LogSink can implement to support 72 | // logging through the slog.Logger or slog.Handler APIs better. It then should 73 | // also support special slog values like slog.Group. When used as a 74 | // slog.Handler, the advantages are: 75 | // 76 | // - stack unwinding gets avoided in favor of logging the pre-recorded PC, 77 | // as intended by slog 78 | // - proper grouping of key/value pairs via WithGroup 79 | // - verbosity levels > slog.LevelInfo can be recorded 80 | // - less overhead 81 | // 82 | // Both APIs (Logger and slog.Logger/Handler) then are supported equally 83 | // well. Developers can pick whatever API suits them better and/or mix 84 | // packages which use either API in the same binary with a common logging 85 | // implementation. 86 | // 87 | // This interface is necessary because the type implementing the LogSink 88 | // interface cannot also implement the slog.Handler interface due to the 89 | // different prototype of the common Enabled method. 90 | // 91 | // An implementation could support both interfaces in two different types, but then 92 | // additional interfaces would be needed to convert between those types in FromSlogHandler 93 | // and ToSlogHandler. 94 | type SlogSink interface { 95 | LogSink 96 | 97 | Handle(ctx context.Context, record slog.Record) error 98 | WithAttrs(attrs []slog.Attr) SlogSink 99 | WithGroup(name string) SlogSink 100 | } 101 | -------------------------------------------------------------------------------- /slogr/slogr.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Package slogr enables usage of a slog.Handler with logr.Logger as front-end 21 | // API and of a logr.LogSink through the slog.Handler and thus slog.Logger 22 | // APIs. 23 | // 24 | // See the README in the top-level [./logr] package for a discussion of 25 | // interoperability. 26 | // 27 | // Deprecated: use the main logr package instead. 28 | package slogr 29 | 30 | import ( 31 | "log/slog" 32 | 33 | "github.com/go-logr/logr" 34 | ) 35 | 36 | // NewLogr returns a logr.Logger which writes to the slog.Handler. 37 | // 38 | // Deprecated: use [logr.FromSlogHandler] instead. 39 | func NewLogr(handler slog.Handler) logr.Logger { 40 | return logr.FromSlogHandler(handler) 41 | } 42 | 43 | // NewSlogHandler returns a slog.Handler which writes to the same sink as the logr.Logger. 44 | // 45 | // Deprecated: use [logr.ToSlogHandler] instead. 46 | func NewSlogHandler(logger logr.Logger) slog.Handler { 47 | return logr.ToSlogHandler(logger) 48 | } 49 | 50 | // ToSlogHandler returns a slog.Handler which writes to the same sink as the logr.Logger. 51 | // 52 | // Deprecated: use [logr.ToSlogHandler] instead. 53 | func ToSlogHandler(logger logr.Logger) slog.Handler { 54 | return logr.ToSlogHandler(logger) 55 | } 56 | 57 | // SlogSink is an optional interface that a LogSink can implement to support 58 | // logging through the slog.Logger or slog.Handler APIs better. 59 | // 60 | // Deprecated: use [logr.SlogSink] instead. 61 | type SlogSink = logr.SlogSink 62 | -------------------------------------------------------------------------------- /slogr_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package logr 21 | 22 | import ( 23 | "bytes" 24 | "fmt" 25 | "io" 26 | "log/slog" 27 | "os" 28 | "path" 29 | "runtime" 30 | "strings" 31 | "testing" 32 | 33 | "github.com/go-logr/logr/internal/testhelp" 34 | ) 35 | 36 | func TestToSlogHandler(t *testing.T) { 37 | t.Run("from simple Logger", func(t *testing.T) { 38 | logger := New(&testLogSink{}) 39 | handler := ToSlogHandler(logger) 40 | if _, ok := handler.(*slogHandler); !ok { 41 | t.Errorf("expected type *slogHandler, got %T", handler) 42 | } 43 | }) 44 | 45 | t.Run("from slog-enabled Logger", func(t *testing.T) { 46 | logger := New(&testSlogSink{}) 47 | handler := ToSlogHandler(logger) 48 | if _, ok := handler.(*slogHandler); !ok { 49 | t.Errorf("expected type *slogHandler, got %T", handler) 50 | } 51 | }) 52 | 53 | t.Run("from slogSink Logger", func(t *testing.T) { 54 | logger := New(&slogSink{handler: slog.NewJSONHandler(os.Stderr, nil)}) 55 | handler := ToSlogHandler(logger) 56 | if _, ok := handler.(*slog.JSONHandler); !ok { 57 | t.Errorf("expected type *slog.JSONHandler, got %T", handler) 58 | } 59 | }) 60 | } 61 | 62 | func TestFromSlogHandler(t *testing.T) { 63 | t.Run("from slog Handler", func(t *testing.T) { 64 | handler := slog.NewJSONHandler(os.Stderr, nil) 65 | logger := FromSlogHandler(handler) 66 | if _, ok := logger.sink.(*slogSink); !ok { 67 | t.Errorf("expected type *slogSink, got %T", logger.sink) 68 | } 69 | }) 70 | 71 | t.Run("from simple slogHandler Handler", func(t *testing.T) { 72 | handler := &slogHandler{sink: &testLogSink{}} 73 | logger := FromSlogHandler(handler) 74 | if _, ok := logger.sink.(*testLogSink); !ok { 75 | t.Errorf("expected type *testSlogSink, got %T", logger.sink) 76 | } 77 | }) 78 | 79 | t.Run("from discard slogHandler Handler", func(t *testing.T) { 80 | handler := &slogHandler{} 81 | logger := FromSlogHandler(handler) 82 | if logger != Discard() { 83 | t.Errorf("expected type *testSlogSink, got %T", logger.sink) 84 | } 85 | }) 86 | } 87 | 88 | var debugWithoutTime = &slog.HandlerOptions{ 89 | ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { 90 | if a.Key == "time" { 91 | return slog.Attr{} 92 | } 93 | return a 94 | }, 95 | Level: slog.LevelDebug, 96 | } 97 | 98 | func TestWithCallDepth(t *testing.T) { 99 | debugWithCaller := *debugWithoutTime 100 | debugWithCaller.AddSource = true 101 | var buffer bytes.Buffer 102 | logger := FromSlogHandler(slog.NewTextHandler(&buffer, &debugWithCaller)) 103 | 104 | logHelper := func(logger Logger) { 105 | logger.WithCallDepth(1).Info("hello") 106 | } 107 | 108 | logHelper(logger) 109 | _, file, line, _ := runtime.Caller(0) 110 | expectedSource := fmt.Sprintf("%s:%d", path.Base(file), line-1) 111 | actual := buffer.String() 112 | if !strings.Contains(actual, expectedSource) { 113 | t.Errorf("expected log entry with %s as caller source code location, got instead:\n%s", expectedSource, actual) 114 | } 115 | } 116 | 117 | func TestRunSlogTestsOnNaiveSlogHandler(t *testing.T) { 118 | // This proves that slogHandler passes slog's own tests when given a 119 | // LogSink which does not implement SlogSink. 120 | exceptions := []string{ 121 | // logr sinks handle time themselves 122 | "a Handler should ignore a zero Record.Time", 123 | // slogHandler does not do groups "properly", so these all fail with 124 | // "missing group". It's looking for `"G":{"a":"b"}` and getting 125 | // `"G.a": "b"`. 126 | // 127 | // NOTE: These make a weird coupling to Go versions. Newer Go versions 128 | // don't need some of these exceptions, but older ones do. It's unclear 129 | // if that is because something changed in slog or if the test was 130 | // removed. 131 | "a Handler should handle Group attributes", 132 | "a Handler should handle the WithGroup method", 133 | "a Handler should handle multiple WithGroup and WithAttr calls", 134 | "a Handler should not output groups for an empty Record", 135 | "a Handler should not output groups if there are no attributes", 136 | "a Handler should not output nested groups if there are no attributes", 137 | "a Handler should call Resolve on attribute values in groups", 138 | "a Handler should call Resolve on attribute values in groups from WithAttrs", 139 | } 140 | testhelp.RunSlogTests(t, func(buffer *bytes.Buffer) slog.Handler { 141 | // We want a known-good Logger that emits JSON but is not a slogHandler 142 | // or SlogSink (since those get special treatment). We can trust that 143 | // the slog JSONHandler works. 144 | handler := slog.NewJSONHandler(buffer, nil) 145 | sink := &passthruLogSink{handler: handler} // passthruLogSink does not implement SlogSink. 146 | logger := New(sink) 147 | return ToSlogHandler(logger) 148 | }, exceptions...) 149 | } 150 | 151 | func TestRunSlogTestsOnEnlightenedSlogHandler(t *testing.T) { 152 | // This proves that slogHandler passes slog's own tests when given a 153 | // LogSink which implements SlogSink. 154 | exceptions := []string{} 155 | testhelp.RunSlogTests(t, func(buffer *bytes.Buffer) slog.Handler { 156 | // We want a known-good Logger that emits JSON and implements SlogSink, 157 | // to cover those paths. We can trust that the slog JSONHandler works. 158 | handler := slog.NewJSONHandler(buffer, nil) 159 | sink := &passthruSlogSink{handler: handler} // passthruSlogSink implements SlogSink. 160 | logger := New(sink) 161 | return ToSlogHandler(logger) 162 | }, exceptions...) 163 | } 164 | 165 | func TestSlogSinkOnDiscard(_ *testing.T) { 166 | // Compile-test 167 | logger := slog.New(ToSlogHandler(Discard())) 168 | logger.WithGroup("foo").With("x", 1).Info("hello") 169 | } 170 | 171 | func TestConversion(t *testing.T) { 172 | d := Discard() 173 | d2 := FromSlogHandler(ToSlogHandler(d)) 174 | expectEqual(t, d, d2) 175 | 176 | e := Logger{} 177 | e2 := FromSlogHandler(ToSlogHandler(e)) 178 | expectEqual(t, e, e2) 179 | 180 | text := slog.NewTextHandler(io.Discard, nil) 181 | text2 := ToSlogHandler(FromSlogHandler(text)) 182 | expectEqual(t, text, text2) 183 | 184 | text3 := ToSlogHandler(FromSlogHandler(text).V(1)) 185 | if handler, ok := text3.(interface { 186 | GetLevel() slog.Level 187 | }); ok { 188 | expectEqual(t, handler.GetLevel(), slog.Level(1)) 189 | } else { 190 | t.Errorf("Expected a slogHandler which implements V(1), got instead: %T %+v", text3, text3) 191 | } 192 | } 193 | 194 | func expectEqual(t *testing.T, expected, actual any) { 195 | if expected != actual { 196 | t.Helper() 197 | t.Errorf("Expected %T %+v, got instead: %T %+v", expected, expected, actual, actual) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /slogsink.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package logr 21 | 22 | import ( 23 | "context" 24 | "log/slog" 25 | "runtime" 26 | "time" 27 | ) 28 | 29 | var ( 30 | _ LogSink = &slogSink{} 31 | _ CallDepthLogSink = &slogSink{} 32 | _ Underlier = &slogSink{} 33 | ) 34 | 35 | // Underlier is implemented by the LogSink returned by NewFromLogHandler. 36 | type Underlier interface { 37 | // GetUnderlying returns the Handler used by the LogSink. 38 | GetUnderlying() slog.Handler 39 | } 40 | 41 | const ( 42 | // nameKey is used to log the `WithName` values as an additional attribute. 43 | nameKey = "logger" 44 | 45 | // errKey is used to log the error parameter of Error as an additional attribute. 46 | errKey = "err" 47 | ) 48 | 49 | type slogSink struct { 50 | callDepth int 51 | name string 52 | handler slog.Handler 53 | } 54 | 55 | func (l *slogSink) Init(info RuntimeInfo) { 56 | l.callDepth = info.CallDepth 57 | } 58 | 59 | func (l *slogSink) GetUnderlying() slog.Handler { 60 | return l.handler 61 | } 62 | 63 | func (l *slogSink) WithCallDepth(depth int) LogSink { 64 | newLogger := *l 65 | newLogger.callDepth += depth 66 | return &newLogger 67 | } 68 | 69 | func (l *slogSink) Enabled(level int) bool { 70 | return l.handler.Enabled(context.Background(), slog.Level(-level)) 71 | } 72 | 73 | func (l *slogSink) Info(level int, msg string, kvList ...interface{}) { 74 | l.log(nil, msg, slog.Level(-level), kvList...) 75 | } 76 | 77 | func (l *slogSink) Error(err error, msg string, kvList ...interface{}) { 78 | l.log(err, msg, slog.LevelError, kvList...) 79 | } 80 | 81 | func (l *slogSink) log(err error, msg string, level slog.Level, kvList ...interface{}) { 82 | var pcs [1]uintptr 83 | // skip runtime.Callers, this function, Info/Error, and all helper functions above that. 84 | runtime.Callers(3+l.callDepth, pcs[:]) 85 | 86 | record := slog.NewRecord(time.Now(), level, msg, pcs[0]) 87 | if l.name != "" { 88 | record.AddAttrs(slog.String(nameKey, l.name)) 89 | } 90 | if err != nil { 91 | record.AddAttrs(slog.Any(errKey, err)) 92 | } 93 | record.Add(kvList...) 94 | _ = l.handler.Handle(context.Background(), record) 95 | } 96 | 97 | func (l slogSink) WithName(name string) LogSink { 98 | if l.name != "" { 99 | l.name += "/" 100 | } 101 | l.name += name 102 | return &l 103 | } 104 | 105 | func (l slogSink) WithValues(kvList ...interface{}) LogSink { 106 | l.handler = l.handler.WithAttrs(kvListToAttrs(kvList...)) 107 | return &l 108 | } 109 | 110 | func kvListToAttrs(kvList ...interface{}) []slog.Attr { 111 | // We don't need the record itself, only its Add method. 112 | record := slog.NewRecord(time.Time{}, 0, "", 0) 113 | record.Add(kvList...) 114 | attrs := make([]slog.Attr, 0, record.NumAttrs()) 115 | record.Attrs(func(attr slog.Attr) bool { 116 | attrs = append(attrs, attr) 117 | return true 118 | }) 119 | return attrs 120 | } 121 | -------------------------------------------------------------------------------- /testimpls_slog_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package logr 21 | 22 | import ( 23 | "context" 24 | "log/slog" 25 | "time" 26 | ) 27 | 28 | var _ SlogSink = &testSlogSink{} 29 | 30 | // testSlogSink is a trivial SlogSink implementation, just for testing, which 31 | // calls (optional) hooks on each method. 32 | type testSlogSink struct { 33 | // embed a plain LogSink 34 | testLogSink 35 | 36 | attrs []slog.Attr 37 | groups []string 38 | 39 | fnHandle func(ss *testSlogSink, ctx context.Context, record slog.Record) 40 | fnWithAttrs func(ss *testSlogSink, attrs []slog.Attr) 41 | fnWithGroup func(ss *testSlogSink, name string) 42 | } 43 | 44 | func (ss *testSlogSink) Handle(ctx context.Context, record slog.Record) error { 45 | if ss.fnHandle != nil { 46 | ss.fnHandle(ss, ctx, record) 47 | } 48 | return nil 49 | } 50 | 51 | func (ss *testSlogSink) WithAttrs(attrs []slog.Attr) SlogSink { 52 | if ss.fnWithAttrs != nil { 53 | ss.fnWithAttrs(ss, attrs) 54 | } 55 | out := *ss 56 | n := len(out.attrs) 57 | out.attrs = append(out.attrs[:n:n], attrs...) 58 | return &out 59 | } 60 | 61 | func (ss *testSlogSink) WithGroup(name string) SlogSink { 62 | if ss.fnWithGroup != nil { 63 | ss.fnWithGroup(ss, name) 64 | } 65 | out := *ss 66 | n := len(out.groups) 67 | out.groups = append(out.groups[:n:n], name) 68 | return &out 69 | } 70 | 71 | // passthruLogSink is a trivial LogSink implementation, which implements the 72 | // logr.LogSink methods in terms of a slog.Handler. 73 | type passthruLogSink struct { 74 | handler slog.Handler 75 | } 76 | 77 | func (pl passthruLogSink) Init(RuntimeInfo) {} 78 | 79 | func (pl passthruLogSink) Enabled(int) bool { return true } 80 | 81 | func (pl passthruLogSink) Error(_ error, msg string, kvList ...interface{}) { 82 | var record slog.Record 83 | record.Message = msg 84 | record.Level = slog.LevelError 85 | record.Time = time.Now() 86 | record.Add(kvList...) 87 | _ = pl.handler.Handle(context.Background(), record) 88 | } 89 | 90 | func (pl passthruLogSink) Info(_ int, msg string, kvList ...interface{}) { 91 | var record slog.Record 92 | record.Message = msg 93 | record.Level = slog.LevelInfo 94 | record.Time = time.Now() 95 | record.Add(kvList...) 96 | _ = pl.handler.Handle(context.Background(), record) 97 | } 98 | 99 | func (pl passthruLogSink) WithName(string) LogSink { return &pl } 100 | 101 | func (pl passthruLogSink) WithValues(kvList ...interface{}) LogSink { 102 | var values slog.Record 103 | values.Add(kvList...) 104 | var attrs []slog.Attr 105 | add := func(attr slog.Attr) bool { 106 | attrs = append(attrs, attr) 107 | return true 108 | } 109 | values.Attrs(add) 110 | 111 | pl.handler = pl.handler.WithAttrs(attrs) 112 | return &pl 113 | } 114 | 115 | // passthruSlogSink is a trivial SlogSink implementation, which stubs out the 116 | // logr.LogSink methods and passes Logr.SlogSink thru to a slog.Handler. 117 | type passthruSlogSink struct { 118 | handler slog.Handler 119 | } 120 | 121 | func (ps passthruSlogSink) Init(RuntimeInfo) {} 122 | func (ps passthruSlogSink) Enabled(int) bool { return true } 123 | func (ps passthruSlogSink) Error(error, string, ...interface{}) {} 124 | func (ps passthruSlogSink) Info(int, string, ...interface{}) {} 125 | func (ps passthruSlogSink) WithName(string) LogSink { return &ps } 126 | func (ps passthruSlogSink) WithValues(...interface{}) LogSink { return &ps } 127 | 128 | func (ps *passthruSlogSink) Handle(ctx context.Context, record slog.Record) error { 129 | return ps.handler.Handle(ctx, record) 130 | } 131 | 132 | func (ps passthruSlogSink) WithAttrs(attrs []slog.Attr) SlogSink { 133 | ps.handler = ps.handler.WithAttrs(attrs) 134 | return &ps 135 | } 136 | 137 | func (ps passthruSlogSink) WithGroup(name string) SlogSink { 138 | ps.handler = ps.handler.WithGroup(name) 139 | return &ps 140 | } 141 | -------------------------------------------------------------------------------- /testimpls_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logr 18 | 19 | // testLogSink is a trivial LogSink implementation, just for testing, which 20 | // calls (optional) hooks on each method. 21 | type testLogSink struct { 22 | fnInit func(ri RuntimeInfo) 23 | fnEnabled func(lvl int) bool 24 | fnInfo func(lvl int, msg string, kv ...any) 25 | fnError func(err error, msg string, kv ...any) 26 | fnWithValues func(kv ...any) 27 | fnWithName func(name string) 28 | 29 | withValues []any 30 | } 31 | 32 | var _ LogSink = &testLogSink{} 33 | 34 | func (ls *testLogSink) Init(ri RuntimeInfo) { 35 | if ls.fnInit != nil { 36 | ls.fnInit(ri) 37 | } 38 | } 39 | 40 | func (ls *testLogSink) Enabled(lvl int) bool { 41 | if ls.fnEnabled != nil { 42 | return ls.fnEnabled(lvl) 43 | } 44 | return false 45 | } 46 | 47 | func (ls *testLogSink) Info(lvl int, msg string, kv ...any) { 48 | if ls.fnInfo != nil { 49 | ls.fnInfo(lvl, msg, kv...) 50 | } 51 | } 52 | 53 | func (ls *testLogSink) Error(err error, msg string, kv ...any) { 54 | if ls.fnError != nil { 55 | ls.fnError(err, msg, kv...) 56 | } 57 | } 58 | 59 | func (ls *testLogSink) WithValues(kv ...any) LogSink { 60 | if ls.fnWithValues != nil { 61 | ls.fnWithValues(kv...) 62 | } 63 | out := *ls 64 | n := len(out.withValues) 65 | out.withValues = append(out.withValues[:n:n], kv...) 66 | return &out 67 | } 68 | 69 | func (ls *testLogSink) WithName(name string) LogSink { 70 | if ls.fnWithName != nil { 71 | ls.fnWithName(name) 72 | } 73 | out := *ls 74 | return &out 75 | } 76 | 77 | type testCallDepthLogSink struct { 78 | testLogSink 79 | callDepth int 80 | fnWithCallDepth func(depth int) 81 | } 82 | 83 | var _ CallDepthLogSink = &testCallDepthLogSink{} 84 | 85 | func (ls *testCallDepthLogSink) WithCallDepth(depth int) LogSink { 86 | if ls.fnWithCallDepth != nil { 87 | ls.fnWithCallDepth(depth) 88 | } 89 | out := *ls 90 | out.callDepth += depth 91 | return &out 92 | } 93 | -------------------------------------------------------------------------------- /testing/test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package testing provides support for using logr in tests. 18 | // Deprecated. See github.com/go-logr/logr/testr instead. 19 | package testing 20 | 21 | import "github.com/go-logr/logr/testr" 22 | 23 | // NewTestLogger returns a logr.Logger that prints through a testing.T object. 24 | // Deprecated. See github.com/go-logr/logr/testr.New instead. 25 | var NewTestLogger = testr.New 26 | 27 | // Options carries parameters which influence the way logs are generated. 28 | // Deprecated. See github.com/go-logr/logr/testr.Options instead. 29 | type Options = testr.Options 30 | 31 | // NewTestLoggerWithOptions returns a logr.Logger that prints through a testing.T object. 32 | // Deprecated. See github.com/go-logr/logr/testr.NewWithOptions instead. 33 | var NewTestLoggerWithOptions = testr.NewWithOptions 34 | 35 | // Underlier exposes access to the underlying testing.T instance. 36 | // Deprecated. See github.com/go-logr/logr/testr.Underlier instead. 37 | type Underlier = testr.Underlier 38 | -------------------------------------------------------------------------------- /testing/test_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package testing 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/go-logr/logr" 24 | ) 25 | 26 | func TestLogger(t *testing.T) { 27 | log := NewTestLogger(t) 28 | log.Info("info") 29 | log.V(0).Info("V(0).info") 30 | log.V(1).Info("v(1).info") 31 | log.Error(fmt.Errorf("error"), "error") 32 | log.WithName("testing").Info("with prefix") 33 | Helper(log, "hello world") 34 | 35 | log = NewTestLoggerWithOptions(t, Options{ 36 | LogTimestamp: true, 37 | Verbosity: 1, 38 | }) 39 | log.V(1).Info("v(1).info with options") 40 | } 41 | 42 | func Helper(log logr.Logger, msg string) { 43 | helper, log := log.WithCallStackHelper() 44 | helper() 45 | helper2(log, msg) 46 | } 47 | 48 | func helper2(log logr.Logger, msg string) { 49 | helper, log := log.WithCallStackHelper() 50 | helper() 51 | log.Info(msg) 52 | } 53 | -------------------------------------------------------------------------------- /testr/testr.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package testr provides support for using logr in tests. 18 | package testr 19 | 20 | import ( 21 | "testing" 22 | 23 | "github.com/go-logr/logr" 24 | "github.com/go-logr/logr/funcr" 25 | ) 26 | 27 | // New returns a logr.Logger that prints through a testing.T object. 28 | // Info logs are only enabled at V(0). 29 | func New(t *testing.T) logr.Logger { 30 | return NewWithOptions(t, Options{}) 31 | } 32 | 33 | // Options carries parameters which influence the way logs are generated. 34 | type Options struct { 35 | // LogTimestamp tells the logger to add a "ts" key to log 36 | // lines. This has some overhead, so some users might not want 37 | // it. 38 | LogTimestamp bool 39 | 40 | // Verbosity tells the logger which V logs to be write. 41 | // Higher values enable more logs. 42 | Verbosity int 43 | } 44 | 45 | // NewWithOptions returns a logr.Logger that prints through a testing.T object. 46 | // In contrast to the simpler New, output formatting can be configured. 47 | func NewWithOptions(t *testing.T, opts Options) logr.Logger { 48 | l := &testlogger{ 49 | testloggerInterface: newLoggerInterfaceWithOptions(t, opts), 50 | } 51 | return logr.New(l) 52 | } 53 | 54 | // TestingT is an interface wrapper around testing.T, testing.B and testing.F. 55 | type TestingT interface { 56 | Helper() 57 | Log(args ...any) 58 | } 59 | 60 | // NewWithInterface returns a logr.Logger that prints through a 61 | // TestingT object. 62 | // In contrast to the simpler New, output formatting can be configured. 63 | func NewWithInterface(t TestingT, opts Options) logr.Logger { 64 | l := newLoggerInterfaceWithOptions(t, opts) 65 | return logr.New(&l) 66 | } 67 | 68 | func newLoggerInterfaceWithOptions(t TestingT, opts Options) testloggerInterface { 69 | return testloggerInterface{ 70 | t: t, 71 | Formatter: funcr.NewFormatter(funcr.Options{ 72 | LogTimestamp: opts.LogTimestamp, 73 | Verbosity: opts.Verbosity, 74 | }), 75 | } 76 | } 77 | 78 | // Underlier exposes access to the underlying testing.T instance. Since 79 | // callers only have a logr.Logger, they have to know which 80 | // implementation is in use, so this interface is less of an 81 | // abstraction and more of a way to test type conversion. 82 | type Underlier interface { 83 | GetUnderlying() *testing.T 84 | } 85 | 86 | // UnderlierInterface exposes access to the underlying TestingT instance. Since 87 | // callers only have a logr.Logger, they have to know which 88 | // implementation is in use, so this interface is less of an 89 | // abstraction and more of a way to test type conversion. 90 | type UnderlierInterface interface { 91 | GetUnderlying() TestingT 92 | } 93 | 94 | // Info logging implementation shared between testLogger and testLoggerInterface. 95 | func logInfo(t TestingT, formatInfo func(int, string, []any) (string, string), level int, msg string, kvList ...any) { 96 | prefix, args := formatInfo(level, msg, kvList) 97 | t.Helper() 98 | if prefix != "" { 99 | args = prefix + ": " + args 100 | } 101 | t.Log(args) 102 | } 103 | 104 | // Error logging implementation shared between testLogger and testLoggerInterface. 105 | func logError(t TestingT, formatError func(error, string, []any) (string, string), err error, msg string, kvList ...any) { 106 | prefix, args := formatError(err, msg, kvList) 107 | t.Helper() 108 | if prefix != "" { 109 | args = prefix + ": " + args 110 | } 111 | t.Log(args) 112 | } 113 | 114 | // This type exists to wrap and modify the method-set of testloggerInterface. 115 | // In particular, it changes the GetUnderlying() method. 116 | type testlogger struct { 117 | testloggerInterface 118 | } 119 | 120 | func (l testlogger) GetUnderlying() *testing.T { 121 | // This method is defined on testlogger, so the only type this could 122 | // possibly be is testing.T, even though that's not guaranteed by the type 123 | // system itself. 124 | return l.t.(*testing.T) //nolint:forcetypeassert 125 | } 126 | 127 | type testloggerInterface struct { 128 | funcr.Formatter 129 | t TestingT 130 | } 131 | 132 | func (l testloggerInterface) WithName(name string) logr.LogSink { 133 | l.AddName(name) // via Formatter 134 | return &l 135 | } 136 | 137 | func (l testloggerInterface) WithValues(kvList ...any) logr.LogSink { 138 | l.AddValues(kvList) // via Formatter 139 | return &l 140 | } 141 | 142 | func (l testloggerInterface) GetCallStackHelper() func() { 143 | return l.t.Helper 144 | } 145 | 146 | func (l testloggerInterface) Info(level int, msg string, kvList ...any) { 147 | l.t.Helper() 148 | logInfo(l.t, l.FormatInfo, level, msg, kvList...) 149 | } 150 | 151 | func (l testloggerInterface) Error(err error, msg string, kvList ...any) { 152 | l.t.Helper() 153 | logError(l.t, l.FormatError, err, msg, kvList...) 154 | } 155 | 156 | func (l testloggerInterface) GetUnderlying() TestingT { 157 | return l.t 158 | } 159 | 160 | // Assert conformance to the interfaces. 161 | var _ logr.LogSink = &testlogger{} 162 | var _ logr.CallStackHelperLogSink = &testlogger{} 163 | var _ Underlier = &testlogger{} 164 | 165 | var _ logr.LogSink = &testloggerInterface{} 166 | var _ logr.CallStackHelperLogSink = &testloggerInterface{} 167 | var _ UnderlierInterface = &testloggerInterface{} 168 | -------------------------------------------------------------------------------- /testr/testr_fuzz_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | /* 5 | Copyright 2022 The logr Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package testr 21 | 22 | import "testing" 23 | 24 | func TestLoggerTestingF(_ *testing.T) { 25 | f := &testing.F{} 26 | _ = NewWithInterface(f, Options{}) 27 | } 28 | -------------------------------------------------------------------------------- /testr/testr_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The logr Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package testr 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/go-logr/logr" 24 | ) 25 | 26 | func TestLogger(t *testing.T) { 27 | log := New(t) 28 | log.Info("info") 29 | log.V(0).Info("V(0).info") 30 | log.V(1).Info("v(1).info") 31 | log.Error(fmt.Errorf("error"), "error") 32 | log.WithName("testing").WithValues("value", "test").Info("with prefix") 33 | log.WithName("testing").Error(fmt.Errorf("error"), "with prefix") 34 | Helper(log, "hello world") 35 | 36 | log = NewWithOptions(t, Options{ 37 | LogTimestamp: true, 38 | Verbosity: 1, 39 | }) 40 | log.V(1).Info("v(1).info with options") 41 | 42 | underlier, ok := log.GetSink().(Underlier) 43 | if !ok { 44 | t.Fatal("couldn't get underlier") 45 | } 46 | if t != underlier.GetUnderlying() { 47 | t.Error("invalid underlier") 48 | } 49 | } 50 | 51 | func TestLoggerInterface(t *testing.T) { 52 | log := NewWithInterface(t, Options{}) 53 | log.Info("info") 54 | log.V(0).Info("V(0).info") 55 | log.V(1).Info("v(1).info") 56 | log.Error(fmt.Errorf("error"), "error") 57 | log.WithName("testing").WithValues("value", "test").Info("with prefix") 58 | log.WithName("testing").Error(fmt.Errorf("error"), "with prefix") 59 | Helper(log, "hello world") 60 | 61 | underlier, ok := log.GetSink().(UnderlierInterface) 62 | if !ok { 63 | t.Fatal("couldn't get underlier") 64 | } 65 | underlierT, ok := underlier.GetUnderlying().(*testing.T) 66 | if !ok { 67 | t.Fatal("couldn't get underlying *testing.T") 68 | } 69 | if t != underlierT { 70 | t.Error("invalid underlier") 71 | } 72 | } 73 | 74 | func TestLoggerTestingB(_ *testing.T) { 75 | b := &testing.B{} 76 | _ = NewWithInterface(b, Options{}) 77 | } 78 | 79 | func Helper(log logr.Logger, msg string) { 80 | helper, log := log.WithCallStackHelper() 81 | helper() 82 | helper2(log, msg) 83 | } 84 | 85 | func helper2(log logr.Logger, msg string) { 86 | helper, log := log.WithCallStackHelper() 87 | helper() 88 | log.Info(msg) 89 | } 90 | --------------------------------------------------------------------------------