pax_global_header00006660000000000000000000000064147052507430014521gustar00rootroot0000000000000052 comment=7fefd766a73221cac158fad9d231ca360f8f189d golang-github-go-logr-logr-1.4.2/000077500000000000000000000000001470525074300165615ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/.github/000077500000000000000000000000001470525074300201215ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/.github/dependabot.yml000066400000000000000000000001661470525074300227540ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" golang-github-go-logr-logr-1.4.2/.github/workflows/000077500000000000000000000000001470525074300221565ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/.github/workflows/apidiff.yaml000066400000000000000000000016441470525074300244510ustar00rootroot00000000000000name: Run apidiff on: [ pull_request ] permissions: contents: read jobs: apidiff: runs-on: ubuntu-latest if: github.base_ref steps: - name: Install Go uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: go-version: 1.21.x - name: Add GOBIN to PATH run: echo "PATH=$(go env GOPATH)/bin:$PATH" >>$GITHUB_ENV - name: Install dependencies run: GO111MODULE=off go get golang.org/x/exp/cmd/apidiff - name: Checkout old code uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: ref: ${{ github.base_ref }} path: "old" - name: Checkout new code uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: path: "new" - name: APIDiff run: ./_tools/apidiff.sh -d ../old working-directory: "new" golang-github-go-logr-logr-1.4.2/.github/workflows/assign.yaml000066400000000000000000000011521470525074300243250ustar00rootroot00000000000000name: Assign on: issues: types: [opened, reopened] pull_request_target: types: [opened, reopened] permissions: contents: read jobs: assign: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | github.rest.issues.addAssignees({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, assignees: ['thockin', 'pohly'] }) golang-github-go-logr-logr-1.4.2/.github/workflows/lint.yaml000066400000000000000000000014661470525074300240170ustar00rootroot00000000000000name: Run lint on: [ push, pull_request ] permissions: contents: read jobs: lint: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Update Go uses: actions/setup-go@v5.0.1 with: go-version: '>=1.21.0' cache: false - name: Lint uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 with: # version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version version: latest # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true # Read args from .golangci.yaml # args: golang-github-go-logr-logr-1.4.2/.github/workflows/scorecard.yml000066400000000000000000000050361470525074300246520ustar00rootroot00000000000000# This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: '28 21 * * 1' push: branches: [ "master" ] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write steps: - name: "Checkout code" uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # you want to enable the Branch-Protection check on a *public* repository, or # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 with: sarif_file: results.sarif golang-github-go-logr-logr-1.4.2/.github/workflows/tests.yaml000066400000000000000000000012111470525074300241770ustar00rootroot00000000000000name: Run tests on: [ push, pull_request ] permissions: contents: read jobs: test: strategy: matrix: version: [ '1.18', '1.19', '1.20', '1.21.0-rc.4' ] platform: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: - name: Install Go uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: go-version: ${{ matrix.version }} - name: Checkout code uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Build run: go build -v ./... - name: Test run: go test -v -race ./... golang-github-go-logr-logr-1.4.2/.golangci.yaml000066400000000000000000000005711470525074300213110ustar00rootroot00000000000000run: timeout: 1m tests: true linters: disable-all: true enable: - asciicheck - errcheck - forcetypeassert - gocritic - gofmt - goimports - gosimple - govet - ineffassign - misspell - revive - staticcheck - typecheck - unused issues: exclude-use-default: false max-issues-per-linter: 0 max-same-issues: 10 golang-github-go-logr-logr-1.4.2/CHANGELOG.md000066400000000000000000000002141470525074300203670ustar00rootroot00000000000000# CHANGELOG ## v1.0.0-rc1 This is the first logged release. Major changes (including breaking changes) have occurred since earlier tags. golang-github-go-logr-logr-1.4.2/CONTRIBUTING.md000066400000000000000000000011031470525074300210050ustar00rootroot00000000000000# Contributing Logr is open to pull-requests, provided they fit within the intended scope of the project. Specifically, this library aims to be VERY small and minimalist, with no external dependencies. ## Compatibility This project intends to follow [semantic versioning](http://semver.org) and is very strict about compatibility. Any proposed changes MUST follow those rules. ## Performance As a logging library, logr must be as light-weight as possible. Any proposed code change must include results of running the [benchmark](./benchmark) before and after the change. golang-github-go-logr-logr-1.4.2/LICENSE000066400000000000000000000261351470525074300175750ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. golang-github-go-logr-logr-1.4.2/README.md000066400000000000000000000460071470525074300200470ustar00rootroot00000000000000# A minimal logging API for Go [![Go Reference](https://pkg.go.dev/badge/github.com/go-logr/logr.svg)](https://pkg.go.dev/github.com/go-logr/logr) [![Go Report Card](https://goreportcard.com/badge/github.com/go-logr/logr)](https://goreportcard.com/report/github.com/go-logr/logr) [![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) logr offers an(other) opinion on how Go programs and libraries can do logging without becoming coupled to a particular logging implementation. This is not an implementation of logging - it is an API. In fact it is two APIs with two different sets of users. The `Logger` type is intended for application and library authors. It provides a relatively small API which can be used everywhere you want to emit logs. It defers the actual act of writing logs (to files, to stdout, or whatever) to the `LogSink` interface. The `LogSink` interface is intended for logging library implementers. It is a pure interface which can be implemented by logging frameworks to provide the actual logging functionality. This decoupling allows application and library developers to write code in terms of `logr.Logger` (which has very low dependency fan-out) while the implementation of logging is managed "up stack" (e.g. in or near `main()`.) Application developers can then switch out implementations as necessary. Many people assert that libraries should not be logging, and as such efforts like this are pointless. Those people are welcome to convince the authors of the tens-of-thousands of libraries that *DO* write logs that they are all wrong. In the meantime, logr takes a more practical approach. ## Typical usage Somewhere, early in an application's life, it will make a decision about which logging library (implementation) it actually wants to use. Something like: ``` func main() { // ... other setup code ... // Create the "root" logger. We have chosen the "logimpl" implementation, // which takes some initial parameters and returns a logr.Logger. logger := logimpl.New(param1, param2) // ... other setup code ... ``` Most apps will call into other libraries, create structures to govern the flow, etc. The `logr.Logger` object can be passed to these other libraries, stored in structs, or even used as a package-global variable, if needed. For example: ``` app := createTheAppObject(logger) app.Run() ``` Outside of this early setup, no other packages need to know about the choice of implementation. They write logs in terms of the `logr.Logger` that they received: ``` type appObject struct { // ... other fields ... logger logr.Logger // ... other fields ... } func (app *appObject) Run() { app.logger.Info("starting up", "timestamp", time.Now()) // ... app code ... ``` ## Background If the Go standard library had defined an interface for logging, this project probably would not be needed. Alas, here we are. When the Go developers started developing such an interface with [slog](https://github.com/golang/go/issues/56345), they adopted some of the logr design but also left out some parts and changed others: | Feature | logr | slog | |---------|------|------| | High-level API | `Logger` (passed by value) | `Logger` (passed by [pointer](https://github.com/golang/go/issues/59126)) | | Low-level API | `LogSink` | `Handler` | | Stack unwinding | done by `LogSink` | done by `Logger` | | Skipping helper functions | `WithCallDepth`, `WithCallStackHelper` | [not supported by Logger](https://github.com/golang/go/issues/59145) | | Generating a value for logging on demand | `Marshaler` | `LogValuer` | | Log levels | >= 0, higher meaning "less important" | positive and negative, with 0 for "info" and higher meaning "more important" | | Error log entries | always logged, don't have a verbosity level | normal log entries with level >= `LevelError` | | Passing logger via context | `NewContext`, `FromContext` | no API | | Adding a name to a logger | `WithName` | no API | | Modify verbosity of log entries in a call chain | `V` | no API | | Grouping of key/value pairs | not supported | `WithGroup`, `GroupValue` | | Pass context for extracting additional values | no API | API variants like `InfoCtx` | The high-level slog API is explicitly meant to be one of many different APIs that can be layered on top of a shared `slog.Handler`. logr is one such alternative API, with [interoperability](#slog-interoperability) provided by some conversion functions. ### Inspiration Before you consider this package, please read [this blog post by the inimitable Dave Cheney][warning-makes-no-sense]. We really appreciate what he has to say, and it largely aligns with our own experiences. ### Differences from Dave's ideas The main differences are: 1. Dave basically proposes doing away with the notion of a logging API in favor of `fmt.Printf()`. We disagree, especially when you consider things like output locations, timestamps, file and line decorations, and structured logging. This package restricts the logging API to just 2 types of logs: info and error. Info logs are things you want to tell the user which are not errors. Error logs are, well, errors. If your code receives an `error` from a subordinate function call and is logging that `error` *and not returning it*, use error logs. 2. Verbosity-levels on info logs. This gives developers a chance to indicate arbitrary grades of importance for info logs, without assigning names with semantic meaning such as "warning", "trace", and "debug." Superficially this may feel very similar, but the primary difference is the lack of semantics. Because verbosity is a numerical value, it's safe to assume that an app running with higher verbosity means more (and less important) logs will be generated. ## Implementations (non-exhaustive) There are implementations for the following logging libraries: - **a function** (can bridge to non-structured libraries): [funcr](https://github.com/go-logr/logr/tree/master/funcr) - **a testing.T** (for use in Go tests, with JSON-like output): [testr](https://github.com/go-logr/logr/tree/master/testr) - **github.com/google/glog**: [glogr](https://github.com/go-logr/glogr) - **k8s.io/klog** (for Kubernetes): [klogr](https://git.k8s.io/klog/klogr) - **a testing.T** (with klog-like text output): [ktesting](https://git.k8s.io/klog/ktesting) - **go.uber.org/zap**: [zapr](https://github.com/go-logr/zapr) - **log** (the Go standard library logger): [stdr](https://github.com/go-logr/stdr) - **github.com/sirupsen/logrus**: [logrusr](https://github.com/bombsimon/logrusr) - **github.com/wojas/genericr**: [genericr](https://github.com/wojas/genericr) (makes it easy to implement your own backend) - **logfmt** (Heroku style [logging](https://www.brandur.org/logfmt)): [logfmtr](https://github.com/iand/logfmtr) - **github.com/rs/zerolog**: [zerologr](https://github.com/go-logr/zerologr) - **github.com/go-kit/log**: [gokitlogr](https://github.com/tonglil/gokitlogr) (also compatible with github.com/go-kit/kit/log since v0.12.0) - **bytes.Buffer** (writing to a buffer): [bufrlogr](https://github.com/tonglil/buflogr) (useful for ensuring values were logged, like during testing) ## slog interoperability Interoperability goes both ways, using the `logr.Logger` API with a `slog.Handler` and using the `slog.Logger` API with a `logr.LogSink`. `FromSlogHandler` and `ToSlogHandler` convert between a `logr.Logger` and a `slog.Handler`. As usual, `slog.New` can be used to wrap such a `slog.Handler` in the high-level slog API. ### Using a `logr.LogSink` as backend for slog Ideally, a logr sink implementation should support both logr and slog by implementing both the normal logr interface(s) and `SlogSink`. Because of a conflict in the parameters of the common `Enabled` method, it is [not possible to implement both slog.Handler and logr.Sink in the same type](https://github.com/golang/go/issues/59110). If both are supported, log calls can go from the high-level APIs to the backend without the need to convert parameters. `FromSlogHandler` and `ToSlogHandler` can convert back and forth without adding additional wrappers, with one exception: when `Logger.V` was used to adjust the verbosity for a `slog.Handler`, then `ToSlogHandler` has to use a wrapper which adjusts the verbosity for future log calls. Such an implementation should also support values that implement specific interfaces from both packages for logging (`logr.Marshaler`, `slog.LogValuer`, `slog.GroupValue`). logr does not convert those. Not supporting slog has several drawbacks: - Recording source code locations works correctly if the handler gets called through `slog.Logger`, but may be wrong in other cases. That's because a `logr.Sink` does its own stack unwinding instead of using the program counter provided by the high-level API. - slog levels <= 0 can be mapped to logr levels by negating the level without a loss of information. But all slog levels > 0 (e.g. `slog.LevelWarning` as used by `slog.Logger.Warn`) must be mapped to 0 before calling the sink because logr does not support "more important than info" levels. - The slog group concept is supported by prefixing each key in a key/value pair with the group names, separated by a dot. For structured output like JSON it would be better to group the key/value pairs inside an object. - Special slog values and interfaces don't work as expected. - The overhead is likely to be higher. These drawbacks are severe enough that applications using a mixture of slog and logr should switch to a different backend. ### Using a `slog.Handler` as backend for logr Using a plain `slog.Handler` without support for logr works better than the other direction: - All logr verbosity levels can be mapped 1:1 to their corresponding slog level by negating them. - Stack unwinding is done by the `SlogSink` and the resulting program counter is passed to the `slog.Handler`. - Names added via `Logger.WithName` are gathered and recorded in an additional attribute with `logger` as key and the names separated by slash as value. - `Logger.Error` is turned into a log record with `slog.LevelError` as level and an additional attribute with `err` as key, if an error was provided. The main drawback is that `logr.Marshaler` will not be supported. Types should ideally support both `logr.Marshaler` and `slog.Valuer`. If compatibility with logr implementations without slog support is not important, then `slog.Valuer` is sufficient. ### Context support for slog Storing a logger in a `context.Context` is not supported by slog. `NewContextWithSlogLogger` and `FromContextAsSlogLogger` can be used to fill this gap. They store and retrieve a `slog.Logger` pointer under the same context key that is also used by `NewContext` and `FromContext` for `logr.Logger` value. When `NewContextWithSlogLogger` is followed by `FromContext`, the latter will automatically convert the `slog.Logger` to a `logr.Logger`. `FromContextAsSlogLogger` does the same for the other direction. With this approach, binaries which use either slog or logr are as efficient as possible with no unnecessary allocations. This is also why the API stores a `slog.Logger` pointer: when storing a `slog.Handler`, creating a `slog.Logger` on retrieval would need to allocate one. The downside is that switching back and forth needs more allocations. Because logr is the API that is already in use by different packages, in particular Kubernetes, the recommendation is to use the `logr.Logger` API in code which uses contextual logging. An alternative to adding values to a logger and storing that logger in the context is to store the values in the context and to configure a logging backend to extract those values when emitting log entries. This only works when log calls are passed the context, which is not supported by the logr API. With the slog API, it is possible, but not required. https://github.com/veqryn/slog-context is a package for slog which provides additional support code for this approach. It also contains wrappers for the context functions in logr, so developers who prefer to not use the logr APIs directly can use those instead and the resulting code will still be interoperable with logr. ## FAQ ### Conceptual #### Why structured logging? - **Structured logs are more easily queryable**: Since you've got key-value pairs, it's much easier to query your structured logs for particular values by filtering on the contents of a particular key -- think searching request logs for error codes, Kubernetes reconcilers for the name and namespace of the reconciled object, etc. - **Structured logging makes it easier to have cross-referenceable logs**: Similarly to searchability, if you maintain conventions around your keys, it becomes easy to gather all log lines related to a particular concept. - **Structured logs allow better dimensions of filtering**: if you have structure to your logs, you've got more precise control over how much information is logged -- you might choose in a particular configuration to log certain keys but not others, only log lines where a certain key matches a certain value, etc., instead of just having v-levels and names to key off of. - **Structured logs better represent structured data**: sometimes, the data that you want to log is inherently structured (think tuple-link objects.) Structured logs allow you to preserve that structure when outputting. #### Why V-levels? **V-levels give operators an easy way to control the chattiness of log operations**. V-levels provide a way for a given package to distinguish the relative importance or verbosity of a given log message. Then, if a particular logger or package is logging too many messages, the user of the package can simply change the v-levels for that library. #### Why not named levels, like Info/Warning/Error? Read [Dave Cheney's post][warning-makes-no-sense]. Then read [Differences from Dave's ideas](#differences-from-daves-ideas). #### Why not allow format strings, too? **Format strings negate many of the benefits of structured logs**: - They're not easily searchable without resorting to fuzzy searching, regular expressions, etc. - They don't store structured data well, since contents are flattened into a string. - They're not cross-referenceable. - They don't compress easily, since the message is not constant. (Unless you turn positional parameters into key-value pairs with numerical keys, at which point you've gotten key-value logging with meaningless keys.) ### Practical #### Why key-value pairs, and not a map? Key-value pairs are *much* easier to optimize, especially around allocations. Zap (a structured logger that inspired logr's interface) has [performance measurements](https://github.com/uber-go/zap#performance) that show this quite nicely. While the interface ends up being a little less obvious, you get potentially better performance, plus avoid making users type `map[string]string{}` every time they want to log. #### What if my V-levels differ between libraries? That's fine. Control your V-levels on a per-logger basis, and use the `WithName` method to pass different loggers to different libraries. Generally, you should take care to ensure that you have relatively consistent V-levels within a given logger, however, as this makes deciding on what verbosity of logs to request easier. #### But I really want to use a format string! That's not actually a question. Assuming your question is "how do I convert my mental model of logging with format strings to logging with constant messages": 1. Figure out what the error actually is, as you'd write in a TL;DR style, and use that as a message. 2. For every place you'd write a format specifier, look to the word before it, and add that as a key value pair. For instance, consider the following examples (all taken from spots in the Kubernetes codebase): - `klog.V(4).Infof("Client is returning errors: code %v, error %v", responseCode, err)` becomes `logger.Error(err, "client returned an error", "code", responseCode)` - `klog.V(4).Infof("Got a Retry-After %ds response for attempt %d to %v", seconds, retries, url)` becomes `logger.V(4).Info("got a retry-after response when requesting url", "attempt", retries, "after seconds", seconds, "url", url)` If you *really* must use a format string, use it in a key's value, and call `fmt.Sprintf` yourself. For instance: `log.Printf("unable to reflect over type %T")` becomes `logger.Info("unable to reflect over type", "type", fmt.Sprintf("%T"))`. In general though, the cases where this is necessary should be few and far between. #### How do I choose my V-levels? This is basically the only hard constraint: increase V-levels to denote more verbose or more debug-y logs. Otherwise, you can start out with `0` as "you always want to see this", `1` as "common logging that you might *possibly* want to turn off", and `10` as "I would like to performance-test your log collection stack." Then gradually choose levels in between as you need them, working your way down from 10 (for debug and trace style logs) and up from 1 (for chattier info-type logs). For reference, slog pre-defines -4 for debug logs (corresponds to 4 in logr), which matches what is [recommended for Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#what-method-to-use). #### How do I choose my keys? Keys are fairly flexible, and can hold more or less any string value. For best compatibility with implementations and consistency with existing code in other projects, there are a few conventions you should consider. - Make your keys human-readable. - Constant keys are generally a good idea. - Be consistent across your codebase. - Keys should naturally match parts of the message string. - Use lower case for simple keys and [lowerCamelCase](https://en.wiktionary.org/wiki/lowerCamelCase) for more complex ones. Kubernetes is one example of a project that has [adopted that convention](https://github.com/kubernetes/community/blob/HEAD/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments). While key names are mostly unrestricted (and spaces are acceptable), it's generally a good idea to stick to printable ascii characters, or at least match the general character set of your log lines. #### Why should keys be constant values? The point of structured logging is to make later log processing easier. Your keys are, effectively, the schema of each log message. If you use different keys across instances of the same log line, you will make your structured logs much harder to use. `Sprintf()` is for values, not for keys! #### Why is this not a pure interface? The Logger type is implemented as a struct in order to allow the Go compiler to optimize things like high-V `Info` logs that are not triggered. Not all of these implementations are implemented yet, but this structure was suggested as a way to ensure they *can* be implemented. All of the real work is behind the `LogSink` interface. [warning-makes-no-sense]: http://dave.cheney.net/2015/11/05/lets-talk-about-logging golang-github-go-logr-logr-1.4.2/SECURITY.md000066400000000000000000000013271470525074300203550ustar00rootroot00000000000000# Security Policy If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released. You may submit the report in the following ways: - send an email to go-logr-security@googlegroups.com - send us a [private vulnerability report](https://github.com/go-logr/logr/security/advisories/new) Please provide the following information in your report: - A description of the vulnerability and its impact - How to reproduce the issue We ask that you give us 90 days to work on a fix before public exposure. golang-github-go-logr-logr-1.4.2/_tools/000077500000000000000000000000001470525074300200605ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/_tools/apidiff.sh000077500000000000000000000062631470525074300220300ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright 2020 The Kubernetes Authors. # Copyright 2021 The logr Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -o errexit set -o nounset set -o pipefail function usage { local script="$(basename $0)" echo >&2 "Usage: ${script} [-r | -d ] This script should be run at the root of a module. -r Compare the exported API of the local working copy with the exported API of the local repo at the specified branch or tag. -d Compare the exported API of the local working copy with the exported API of the specified directory, which should point to the root of a different version of the same module. Examples: ${script} -r master ${script} -r v1.10.0 ${script} -r release-1.10 ${script} -d /path/to/historical/version " exit 1 } ref="" dir="" while getopts r:d: o do case "$o" in r) ref="$OPTARG";; d) dir="$OPTARG";; [?]) usage;; esac done # If REF and DIR are empty, print usage and error if [[ -z "${ref}" && -z "${dir}" ]]; then usage; fi # If REF and DIR are both set, print usage and error if [[ -n "${ref}" && -n "${dir}" ]]; then usage; fi if ! which apidiff > /dev/null; then echo "Installing golang.org/x/exp/cmd/apidiff" pushd "${TMPDIR:-/tmp}" > /dev/null GO111MODULE=off go get golang.org/x/exp/cmd/apidiff popd > /dev/null fi output=$(mktemp -d -t "apidiff.output.XXXX") cleanup_output () { rm -fr "${output}"; } trap cleanup_output EXIT # If ref is set, clone . to temp dir at $ref, and set $dir to the temp dir clone="" base="${dir}" if [[ -n "${ref}" ]]; then base="${ref}" clone=$(mktemp -d -t "apidiff.clone.XXXX") cleanup_clone_and_output () { rm -fr "${clone}"; cleanup_output; } trap cleanup_clone_and_output EXIT git clone . -q --no-tags "${clone}" git -C "${clone}" co "${ref}" dir="${clone}" fi pushd "${dir}" >/dev/null echo "Inspecting API of ${base}" go list ./... > packages.txt for pkg in $(cat packages.txt); do mkdir -p "${output}/${pkg}" apidiff -w "${output}/${pkg}/apidiff.output" "${pkg}" done popd >/dev/null retval=0 echo "Comparing with ${base}" for pkg in $(go list ./...); do # New packages are ok if [ ! -f "${output}/${pkg}/apidiff.output" ]; then continue fi # Check for incompatible changes to previous packages incompatible=$(apidiff -incompatible "${output}/${pkg}/apidiff.output" "${pkg}") if [[ -n "${incompatible}" ]]; then echo >&2 "FAIL: ${pkg} contains incompatible changes: ${incompatible} " retval=1 fi done # Check for removed packages removed=$(comm -23 "${dir}/packages.txt" <(go list ./...)) if [[ -n "${removed}" ]]; then echo >&2 "FAIL: removed packages: ${removed} " retval=1 fi exit $retval golang-github-go-logr-logr-1.4.2/benchmark/000077500000000000000000000000001470525074300205135ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/benchmark/README.md000066400000000000000000000004701470525074300217730ustar00rootroot00000000000000# Benchmarking logr Any major changes to the logr library must be benchmarked before and after the change. ## Running the benchmark ``` $ go test -bench='.' -test.benchmem ./benchmark/ ``` ## Fixing the benchmark If you think this benchmark can be improved, you are probably correct! PRs are very welcome. golang-github-go-logr-logr-1.4.2/benchmark/benchmark_slog_test.go000066400000000000000000000071051470525074300250620ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "log/slog" "os" "testing" "github.com/go-logr/logr" ) // // slogSink wrapper of discard // func BenchmarkSlogSinkLogInfoOneArg(b *testing.B) { var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) doInfoOneArg(b, log) } func BenchmarkSlogSinkLogInfoSeveralArgs(b *testing.B) { var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) doInfoSeveralArgs(b, log) } func BenchmarkSlogSinkLogInfoWithValues(b *testing.B) { var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) doInfoWithValues(b, log) } func BenchmarkSlogSinkLogV0Info(b *testing.B) { var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) doV0Info(b, log) } func BenchmarkSlogSinkLogV9Info(b *testing.B) { var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) doV9Info(b, log) } func BenchmarkSlogSinkLogError(b *testing.B) { var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) doError(b, log) } func BenchmarkSlogSinkWithValues(b *testing.B) { var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) doWithValues(b, log) } func BenchmarkSlogSinkWithName(b *testing.B) { var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) doWithName(b, log) } // // slogSink wrapper of slog's JSONHandler, for comparison // func makeSlogJSONLogger() logr.Logger { devnull, _ := os.Open("/dev/null") handler := slog.NewJSONHandler(devnull, nil) return logr.FromSlogHandler(handler) } func BenchmarkSlogJSONLogInfoOneArg(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doInfoOneArg(b, log) } func BenchmarkSlogJSONLogInfoSeveralArgs(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doInfoSeveralArgs(b, log) } func BenchmarkSlogJSONLogInfoWithValues(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doInfoWithValues(b, log) } func BenchmarkSlogJSONLogV0Info(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doV0Info(b, log) } func BenchmarkSlogJSONLogV9Info(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doV9Info(b, log) } func BenchmarkSlogJSONLogError(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doError(b, log) } func BenchmarkSlogJSONLogWithValues(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doWithValues(b, log) } func BenchmarkSlogJSONWithName(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doWithName(b, log) } func BenchmarkSlogJSONWithCallDepth(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doWithCallDepth(b, log) } func BenchmarkSlogJSONLogInfoStringerValue(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doStringerValue(b, log) } func BenchmarkSlogJSONLogInfoErrorValue(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doErrorValue(b, log) } func BenchmarkSlogJSONLogInfoMarshalerValue(b *testing.B) { var log logr.Logger = makeSlogJSONLogger() doMarshalerValue(b, log) } golang-github-go-logr-logr-1.4.2/benchmark/benchmark_test.go000066400000000000000000000147201470525074300240370ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "fmt" "testing" "github.com/go-logr/logr" "github.com/go-logr/logr/funcr" ) //go:noinline func doInfoOneArg(b *testing.B, log logr.Logger) { for i := 0; i < b.N; i++ { log.Info("this is", "a", "string") } } //go:noinline func doInfoSeveralArgs(b *testing.B, log logr.Logger) { for i := 0; i < b.N; i++ { log.Info("multi", "bool", true, "string", "str", "int", 42, "float", 3.14, "struct", struct{ X, Y int }{93, 76}) } } //go:noinline func doInfoWithValues(b *testing.B, log logr.Logger) { log = log.WithValues("k1", "str", "k2", 222, "k3", true, "k4", 1.0) for i := 0; i < b.N; i++ { log.Info("multi", "bool", true, "string", "str", "int", 42, "float", 3.14, "struct", struct{ X, Y int }{93, 76}) } } //go:noinline func doV0Info(b *testing.B, log logr.Logger) { for i := 0; i < b.N; i++ { log.V(0).Info("multi", "bool", true, "string", "str", "int", 42, "float", 3.14, "struct", struct{ X, Y int }{93, 76}) } } //go:noinline func doV9Info(b *testing.B, log logr.Logger) { for i := 0; i < b.N; i++ { log.V(9).Info("multi", "bool", true, "string", "str", "int", 42, "float", 3.14, "struct", struct{ X, Y int }{93, 76}) } } //go:noinline func doError(b *testing.B, log logr.Logger) { err := fmt.Errorf("error message") for i := 0; i < b.N; i++ { log.Error(err, "multi", "bool", true, "string", "str", "int", 42, "float", 3.14, "struct", struct{ X, Y int }{93, 76}) } } //go:noinline func doWithValues(b *testing.B, log logr.Logger) { for i := 0; i < b.N; i++ { l := log.WithValues("k1", "v1", "k2", "v2") _ = l } } //go:noinline func doWithName(b *testing.B, log logr.Logger) { for i := 0; i < b.N; i++ { l := log.WithName("name") _ = l } } //go:noinline func doWithCallDepth(b *testing.B, log logr.Logger) { for i := 0; i < b.N; i++ { l := log.WithCallDepth(1) _ = l } } type Tstringer struct{ s string } func (t Tstringer) String() string { return t.s } //go:noinline func doStringerValue(b *testing.B, log logr.Logger) { for i := 0; i < b.N; i++ { log.Info("this is", "a", Tstringer{"stringer"}) } } type Terror struct{ s string } func (t Terror) Error() string { return t.s } //go:noinline func doErrorValue(b *testing.B, log logr.Logger) { for i := 0; i < b.N; i++ { log.Info("this is", "an", Terror{"error"}) } } type Tmarshaler struct{ s string } func (t Tmarshaler) MarshalLog() any { return t.s } //go:noinline func doMarshalerValue(b *testing.B, log logr.Logger) { for i := 0; i < b.N; i++ { log.Info("this is", "a", Tmarshaler{"marshaler"}) } } // // discard // func BenchmarkDiscardLogInfoOneArg(b *testing.B) { var log logr.Logger = logr.Discard() doInfoOneArg(b, log) } func BenchmarkDiscardLogInfoSeveralArgs(b *testing.B) { var log logr.Logger = logr.Discard() doInfoSeveralArgs(b, log) } func BenchmarkDiscardLogInfoWithValues(b *testing.B) { var log logr.Logger = logr.Discard() doInfoWithValues(b, log) } func BenchmarkDiscardLogV0Info(b *testing.B) { var log logr.Logger = logr.Discard() doV0Info(b, log) } func BenchmarkDiscardLogV9Info(b *testing.B) { var log logr.Logger = logr.Discard() doV9Info(b, log) } func BenchmarkDiscardLogError(b *testing.B) { var log logr.Logger = logr.Discard() doError(b, log) } func BenchmarkDiscardWithValues(b *testing.B) { var log logr.Logger = logr.Discard() doWithValues(b, log) } func BenchmarkDiscardWithName(b *testing.B) { var log logr.Logger = logr.Discard() doWithName(b, log) } // // funcr // func noopKV(_, _ string) {} func noopJSON(_ string) {} func BenchmarkFuncrLogInfoOneArg(b *testing.B) { var log logr.Logger = funcr.New(noopKV, funcr.Options{}) doInfoOneArg(b, log) } func BenchmarkFuncrJSONLogInfoOneArg(b *testing.B) { var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) doInfoOneArg(b, log) } func BenchmarkFuncrLogInfoSeveralArgs(b *testing.B) { var log logr.Logger = funcr.New(noopKV, funcr.Options{}) doInfoSeveralArgs(b, log) } func BenchmarkFuncrJSONLogInfoSeveralArgs(b *testing.B) { var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) doInfoSeveralArgs(b, log) } func BenchmarkFuncrLogInfoWithValues(b *testing.B) { var log logr.Logger = funcr.New(noopKV, funcr.Options{}) doInfoWithValues(b, log) } func BenchmarkFuncrJSONLogInfoWithValues(b *testing.B) { var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) doInfoWithValues(b, log) } func BenchmarkFuncrLogV0Info(b *testing.B) { var log logr.Logger = funcr.New(noopKV, funcr.Options{}) doV0Info(b, log) } func BenchmarkFuncrJSONLogV0Info(b *testing.B) { var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) doV0Info(b, log) } func BenchmarkFuncrLogV9Info(b *testing.B) { var log logr.Logger = funcr.New(noopKV, funcr.Options{}) doV9Info(b, log) } func BenchmarkFuncrJSONLogV9Info(b *testing.B) { var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) doV9Info(b, log) } func BenchmarkFuncrLogError(b *testing.B) { var log logr.Logger = funcr.New(noopKV, funcr.Options{}) doError(b, log) } func BenchmarkFuncrJSONLogError(b *testing.B) { var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) doError(b, log) } func BenchmarkFuncrWithValues(b *testing.B) { var log logr.Logger = funcr.New(noopKV, funcr.Options{}) doWithValues(b, log) } func BenchmarkFuncrWithName(b *testing.B) { var log logr.Logger = funcr.New(noopKV, funcr.Options{}) doWithName(b, log) } func BenchmarkFuncrWithCallDepth(b *testing.B) { var log logr.Logger = funcr.New(noopKV, funcr.Options{}) doWithCallDepth(b, log) } func BenchmarkFuncrJSONLogInfoStringerValue(b *testing.B) { var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) doStringerValue(b, log) } func BenchmarkFuncrJSONLogInfoErrorValue(b *testing.B) { var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) doErrorValue(b, log) } func BenchmarkFuncrJSONLogInfoMarshalerValue(b *testing.B) { var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) doMarshalerValue(b, log) } golang-github-go-logr-logr-1.4.2/context.go000066400000000000000000000017671470525074300206070ustar00rootroot00000000000000/* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr // contextKey is how we find Loggers in a context.Context. With Go < 1.21, // the value is always a Logger value. With Go >= 1.21, the value can be a // Logger value or a slog.Logger pointer. type contextKey struct{} // notFoundError exists to carry an IsNotFound method. type notFoundError struct{} func (notFoundError) Error() string { return "no logr.Logger was present" } func (notFoundError) IsNotFound() bool { return true } golang-github-go-logr-logr-1.4.2/context_noslog.go000066400000000000000000000025371470525074300221640ustar00rootroot00000000000000//go:build !go1.21 // +build !go1.21 /* Copyright 2019 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "context" ) // FromContext returns a Logger from ctx or an error if no Logger is found. func FromContext(ctx context.Context) (Logger, error) { if v, ok := ctx.Value(contextKey{}).(Logger); ok { return v, nil } return Logger{}, notFoundError{} } // FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this // returns a Logger that discards all log messages. func FromContextOrDiscard(ctx context.Context) Logger { if v, ok := ctx.Value(contextKey{}).(Logger); ok { return v } return Discard() } // NewContext returns a new Context, derived from ctx, which carries the // provided Logger. func NewContext(ctx context.Context, logger Logger) context.Context { return context.WithValue(ctx, contextKey{}, logger) } golang-github-go-logr-logr-1.4.2/context_slog.go000066400000000000000000000043551470525074300216270ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2019 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "context" "fmt" "log/slog" ) // FromContext returns a Logger from ctx or an error if no Logger is found. func FromContext(ctx context.Context) (Logger, error) { v := ctx.Value(contextKey{}) if v == nil { return Logger{}, notFoundError{} } switch v := v.(type) { case Logger: return v, nil case *slog.Logger: return FromSlogHandler(v.Handler()), nil default: // Not reached. panic(fmt.Sprintf("unexpected value type for logr context key: %T", v)) } } // FromContextAsSlogLogger returns a slog.Logger from ctx or nil if no such Logger is found. func FromContextAsSlogLogger(ctx context.Context) *slog.Logger { v := ctx.Value(contextKey{}) if v == nil { return nil } switch v := v.(type) { case Logger: return slog.New(ToSlogHandler(v)) case *slog.Logger: return v default: // Not reached. panic(fmt.Sprintf("unexpected value type for logr context key: %T", v)) } } // FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this // returns a Logger that discards all log messages. func FromContextOrDiscard(ctx context.Context) Logger { if logger, err := FromContext(ctx); err == nil { return logger } return Discard() } // NewContext returns a new Context, derived from ctx, which carries the // provided Logger. func NewContext(ctx context.Context, logger Logger) context.Context { return context.WithValue(ctx, contextKey{}, logger) } // NewContextWithSlogLogger returns a new Context, derived from ctx, which carries the // provided slog.Logger. func NewContextWithSlogLogger(ctx context.Context, logger *slog.Logger) context.Context { return context.WithValue(ctx, contextKey{}, logger) } golang-github-go-logr-logr-1.4.2/context_slog_test.go000066400000000000000000000035251470525074300226640ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "context" "log/slog" "os" "testing" ) func TestContextWithSlog(t *testing.T) { ctx := context.Background() if out := FromContextAsSlogLogger(ctx); out != nil { t.Errorf("expected no logger, got %#v", out) } // Write as slog... slogger := slog.New(slog.NewJSONHandler(os.Stderr, nil)) sctx := NewContextWithSlogLogger(ctx, slogger) // ...read as logr if out, err := FromContext(sctx); err != nil { t.Errorf("unexpected error: %v", err) } else if _, ok := out.sink.(*slogSink); !ok { t.Errorf("expected output to be type *logr.slogSink, got %T", out.sink) } // ...read as slog if out := FromContextAsSlogLogger(sctx); out == nil { t.Errorf("expected a *slog.JSONHandler, got nil") } else if _, ok := out.Handler().(*slog.JSONHandler); !ok { t.Errorf("expected output to be type *slog.JSONHandler, got %T", out.Handler()) } // Write as logr... logger := Discard() lctx := NewContext(ctx, logger) // ...read as slog if out := FromContextAsSlogLogger(lctx); out == nil { t.Errorf("expected a *log.slogHandler, got nil") } else if _, ok := out.Handler().(*slogHandler); !ok { t.Errorf("expected output to be type *logr.slogHandler, got %T", out.Handler()) } // ...read as logr is covered in the non-slog test } golang-github-go-logr-logr-1.4.2/context_test.go000066400000000000000000000026571470525074300216450ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "context" "testing" ) func TestContext(t *testing.T) { ctx := context.Background() if out, err := FromContext(ctx); err == nil { t.Errorf("expected error, got %#v", out) } else if _, ok := err.(notFoundError); !ok { t.Errorf("expected a notFoundError, got %#v", err) } out := FromContextOrDiscard(ctx) if out.sink != nil { t.Errorf("expected a nil sink, got %#v", out) } sink := &testLogSink{} logger := New(sink) lctx := NewContext(ctx, logger) if out, err := FromContext(lctx); err != nil { t.Errorf("unexpected error: %v", err) } else if p, _ := out.sink.(*testLogSink); p != sink { t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) } out = FromContextOrDiscard(lctx) if p, _ := out.sink.(*testLogSink); p != sink { t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) } } golang-github-go-logr-logr-1.4.2/discard.go000066400000000000000000000015011470525074300205160ustar00rootroot00000000000000/* Copyright 2020 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr // Discard returns a Logger that discards all messages logged to it. It can be // used whenever the caller is not interested in the logs. Logger instances // produced by this function always compare as equal. func Discard() Logger { return New(nil) } golang-github-go-logr-logr-1.4.2/discard_test.go000066400000000000000000000025301470525074300215600ustar00rootroot00000000000000/* Copyright 2020 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "errors" "reflect" "testing" ) func TestDiscard(t *testing.T) { l := Discard() if l.GetSink() != nil { t.Error("did not return the expected underlying type") } // Verify that none of the methods panic, there is not more we can test. l.WithName("discard").WithValues("z", 5).Info("Hello world") l.Info("Hello world", "x", 1, "y", 2) l.V(1).Error(errors.New("foo"), "a", 123) if l.Enabled() { t.Error("discard loggers must always be disabled") } } func TestComparable(t *testing.T) { a := Discard() if !reflect.TypeOf(a).Comparable() { t.Fatal("discard loggers must be comparable") } b := Discard() if a != b { t.Fatal("any two discard Loggers must be equal") } c := Discard().V(2) if b != c { t.Fatal("any two discard Loggers must be equal") } } golang-github-go-logr-logr-1.4.2/example_marshaler_secret_test.go000066400000000000000000000023741470525074300252130ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr_test import ( "github.com/go-logr/logr" ) // ComplexObjectRef contains more fields than it wants to get logged. type ComplexObjectRef struct { Name string Namespace string Secret string } func (ref ComplexObjectRef) MarshalLog() any { return struct { Name, Namespace string }{ Name: ref.Name, Namespace: ref.Namespace, } } var _ logr.Marshaler = ComplexObjectRef{} func ExampleMarshaler_secret() { l := NewStdoutLogger() secret := ComplexObjectRef{Namespace: "kube-system", Name: "some-secret", Secret: "do-not-log-me"} l.Info("simplified", "secret", secret) // Output: // "level"=0 "msg"="simplified" "secret"={"Name"="some-secret" "Namespace"="kube-system"} } golang-github-go-logr-logr-1.4.2/example_marshaler_test.go000066400000000000000000000032101470525074300236340ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr_test import ( "github.com/go-logr/logr" ) // ObjectRef references a Kubernetes object type ObjectRef struct { Name string `json:"name"` Namespace string `json:"namespace,omitempty"` } func (ref ObjectRef) String() string { if ref.Namespace != "" { return ref.Namespace + "/" + ref.Name } return ref.Name } func (ref ObjectRef) MarshalLog() any { // We implement fmt.Stringer for non-structured logging, but we want the // raw struct when using structured logs. Some logr implementations call // String if it is present, so we want to convert this struct to something // that doesn't have that method. type forLog ObjectRef // methods do not survive type definitions return forLog(ref) } var _ logr.Marshaler = ObjectRef{} func ExampleMarshaler() { l := NewStdoutLogger() pod := ObjectRef{Namespace: "kube-system", Name: "some-pod"} l.Info("as string", "pod", pod.String()) l.Info("as struct", "pod", pod) // Output: // "level"=0 "msg"="as string" "pod"="kube-system/some-pod" // "level"=0 "msg"="as struct" "pod"={"name"="some-pod" "namespace"="kube-system"} } golang-github-go-logr-logr-1.4.2/example_slogr_test.go000066400000000000000000000044161470525074300230150ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr_test import ( "errors" "fmt" "log/slog" "os" "github.com/go-logr/logr" "github.com/go-logr/logr/funcr" ) var debugWithoutTime = &slog.HandlerOptions{ ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { if a.Key == "time" { return slog.Attr{} } return a }, Level: slog.LevelDebug, } func ExampleFromSlogHandler() { logrLogger := logr.FromSlogHandler(slog.NewTextHandler(os.Stdout, debugWithoutTime)) logrLogger.Info("hello world") logrLogger.Error(errors.New("fake error"), "ignore me") logrLogger.WithValues("x", 1, "y", 2).WithValues("str", "abc").WithName("foo").WithName("bar").V(4).Info("with values, verbosity and name") // Output: // level=INFO msg="hello world" // level=ERROR msg="ignore me" err="fake error" // level=DEBUG msg="with values, verbosity and name" x=1 y=2 str=abc logger=foo/bar } func ExampleToSlogHandler() { funcrLogger := funcr.New(func(prefix, args string) { if prefix != "" { fmt.Fprintln(os.Stdout, prefix, args) } else { fmt.Fprintln(os.Stdout, args) } }, funcr.Options{ Verbosity: 10, }) slogLogger := slog.New(logr.ToSlogHandler(funcrLogger)) slogLogger.Info("hello world") slogLogger.Error("ignore me", "err", errors.New("fake error")) slogLogger.With("x", 1, "y", 2).WithGroup("group").With("str", "abc").Warn("with values and group") slogLogger = slog.New(logr.ToSlogHandler(funcrLogger.V(int(-slog.LevelDebug)))) slogLogger.Info("info message reduced to debug level") // Output: // "level"=0 "msg"="hello world" // "msg"="ignore me" "error"=null "err"="fake error" // "level"=0 "msg"="with values and group" "x"=1 "y"=2 "group"={"str"="abc"} // "level"=4 "msg"="info message reduced to debug level" } golang-github-go-logr-logr-1.4.2/example_test.go000066400000000000000000000062741470525074300216130ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr_test import ( "fmt" "github.com/go-logr/logr" "github.com/go-logr/logr/funcr" ) // NewStdoutLogger returns a logr.Logger that prints to stdout. func NewStdoutLogger() logr.Logger { return funcr.New(func(prefix, args string) { if prefix != "" { fmt.Printf("%s: %s\n", prefix, args) } else { fmt.Println(args) } }, funcr.Options{}) } func Example() { l := NewStdoutLogger() l.Info("default info log", "stringVal", "value", "intVal", 12345) l.V(0).Info("V(0) info log", "stringVal", "value", "intVal", 12345) l.Error(fmt.Errorf("an error"), "error log", "stringVal", "value", "intVal", 12345) // Output: // "level"=0 "msg"="default info log" "stringVal"="value" "intVal"=12345 // "level"=0 "msg"="V(0) info log" "stringVal"="value" "intVal"=12345 // "msg"="error log" "error"="an error" "stringVal"="value" "intVal"=12345 } func ExampleLogger_Info() { l := NewStdoutLogger() l.Info("this is a V(0)-equivalent info log", "stringVal", "value", "intVal", 12345) // Output: // "level"=0 "msg"="this is a V(0)-equivalent info log" "stringVal"="value" "intVal"=12345 } func ExampleLogger_Error() { l := NewStdoutLogger() l.Error(fmt.Errorf("the error"), "this is an error log", "stringVal", "value", "intVal", 12345) l.Error(nil, "this is an error log with nil error", "stringVal", "value", "intVal", 12345) // Output: // "msg"="this is an error log" "error"="the error" "stringVal"="value" "intVal"=12345 // "msg"="this is an error log with nil error" "error"=null "stringVal"="value" "intVal"=12345 } func ExampleLogger_WithName() { l := NewStdoutLogger() l = l.WithName("name1") l.Info("this is an info log", "stringVal", "value", "intVal", 12345) l = l.WithName("name2") l.Info("this is an info log", "stringVal", "value", "intVal", 12345) // Output: // name1: "level"=0 "msg"="this is an info log" "stringVal"="value" "intVal"=12345 // name1/name2: "level"=0 "msg"="this is an info log" "stringVal"="value" "intVal"=12345 } func ExampleLogger_WithValues() { l := NewStdoutLogger() l = l.WithValues("stringVal", "value", "intVal", 12345) l = l.WithValues("boolVal", true) l.Info("this is an info log", "floatVal", 3.1415) // Output: // "level"=0 "msg"="this is an info log" "stringVal"="value" "intVal"=12345 "boolVal"=true "floatVal"=3.1415 } func ExampleLogger_V() { l := NewStdoutLogger() l.V(0).Info("V(0) info log") l.V(1).Info("V(1) info log") l.V(2).Info("V(2) info log") // Output: // "level"=0 "msg"="V(0) info log" } func ExampleLogger_Enabled() { l := NewStdoutLogger() if loggerV := l.V(5); loggerV.Enabled() { // Do something expensive. loggerV.Info("this is an expensive log message") } // Output: } golang-github-go-logr-logr-1.4.2/examples/000077500000000000000000000000001470525074300203775ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/examples/slog/000077500000000000000000000000001470525074300213435ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/examples/slog/main.go000066400000000000000000000054421470525074300226230ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package main is an example of using slogr. package main import ( "context" "fmt" "log/slog" "os" "github.com/go-logr/logr" "github.com/go-logr/logr/funcr" ) type e struct { str string } func (e e) Error() string { return e.str } func logrHelper(log logr.Logger, msg string) { logrHelper2(log, msg) } func logrHelper2(log logr.Logger, msg string) { log.WithCallDepth(2).Info(msg) } func slogHelper(log *slog.Logger, msg string) { slogHelper2(log, msg) } func slogHelper2(log *slog.Logger, msg string) { // slog.Logger has no API for skipping helper functions, so this gets logged as call location. log.Info(msg) } func main() { opts := slog.HandlerOptions{ AddSource: true, Level: slog.Level(-1), } handler := slog.NewJSONHandler(os.Stderr, &opts) logrLogger := logr.FromSlogHandler(handler) logrExample(logrLogger) logrLogger = funcr.NewJSON( func(obj string) { fmt.Println(obj) }, funcr.Options{ LogCaller: funcr.All, LogTimestamp: true, Verbosity: 1, }) slogLogger := slog.New(logr.ToSlogHandler(logrLogger)) slogExample(slogLogger) } func logrExample(log logr.Logger) { log = log.WithName("my") log = log.WithName("logger") log = log.WithName("name") log = log.WithValues("saved", "value") log.Info("1) hello", "val1", 1, "val2", map[string]int{"k": 1}) log.V(1).Info("2) you should see this") log.V(1).V(1).Info("you should NOT see this") log.Error(nil, "3) uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) log.Error(e{"an error occurred"}, "4) goodbye", "code", -1) logrHelper(log, "5) thru a helper") } func slogExample(log *slog.Logger) { // There's no guarantee that this logs the right source code location. // It works for Go 1.21.0 by compensating in logr.ToSlogHandler // for the additional callers, but those might change. log = log.With("saved", "value") log.Info("1) hello", "val1", 1, "val2", map[string]int{"k": 1}) log.Log(context.TODO(), slog.Level(-1), "2) you should see this") log.Log(context.TODO(), slog.Level(-2), "you should NOT see this") log.Error("3) uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) log.Error("4) goodbye", "code", -1, "err", e{"an error occurred"}) slogHelper(log, "5) thru a helper") } golang-github-go-logr-logr-1.4.2/examples/tab_logger.go000066400000000000000000000045151470525074300230400ustar00rootroot00000000000000/* Copyright 2019 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "os" "text/tabwriter" "github.com/go-logr/logr" ) // tabLogSink is a sample logr.LogSink that logs to stderr. // It's terribly inefficient, and is only a basic example. type tabLogSink struct { name string keyValues map[string]any writer *tabwriter.Writer } var _ logr.LogSink = &tabLogSink{} // Note that Init usually takes a pointer so it can modify the receiver to save // runtime info. func (*tabLogSink) Init(info logr.RuntimeInfo) { } func (tabLogSink) Enabled(level int) bool { return true } func (l tabLogSink) Info(level int, msg string, kvs ...any) { fmt.Fprintf(l.writer, "%s\t%s\t", l.name, msg) for k, v := range l.keyValues { fmt.Fprintf(l.writer, "%s: %+v ", k, v) } for i := 0; i < len(kvs); i += 2 { fmt.Fprintf(l.writer, "%s: %+v ", kvs[i], kvs[i+1]) } fmt.Fprintf(l.writer, "\n") l.writer.Flush() } func (l tabLogSink) Error(err error, msg string, kvs ...any) { kvs = append(kvs, "error", err) l.Info(0, msg, kvs...) } func (l tabLogSink) WithName(name string) logr.LogSink { return &tabLogSink{ name: l.name + "." + name, keyValues: l.keyValues, writer: l.writer, } } func (l tabLogSink) WithValues(kvs ...any) logr.LogSink { newMap := make(map[string]any, len(l.keyValues)+len(kvs)/2) for k, v := range l.keyValues { newMap[k] = v } for i := 0; i < len(kvs); i += 2 { newMap[kvs[i].(string)] = kvs[i+1] } return &tabLogSink{ name: l.name, keyValues: newMap, writer: l.writer, } } // NewTabLogger is the main entry-point to this implementation. App developers // call this somewhere near main() and thenceforth only deal with logr.Logger. func NewTabLogger() logr.Logger { sink := &tabLogSink{ writer: tabwriter.NewWriter(os.Stderr, 40, 8, 2, '\t', 0), } return logr.New(sink) } golang-github-go-logr-logr-1.4.2/examples/usage_example.go000066400000000000000000000102101470525074300235370ustar00rootroot00000000000000/* Copyright 2019 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "math/rand" "time" "github.com/go-logr/logr" ) // This application demonstrates the usage of logger. // It's a simple reconciliation loop that pretends to // receive notifications about updates from a some API // server, make some changes, and then submit updates of // its own. // This uses object-based logging. It's also possible // (but a bit trickier) to use file-level "base" loggers. var objectMap = map[string]Object{ "obj1": { Name: "obj1", Kind: "one", Details: 33, }, "obj2": { Name: "obj2", Kind: "two", Details: "hi", }, "obj3": { Name: "obj3", Kind: "one", Details: 1, }, } // Object is an app contruct that might want to be logged. type Object struct { Name string Kind string Details any } // Client is a simulated client in this example app. type Client struct { objects map[string]Object log logr.Logger } // Get retrieves an object. func (c *Client) Get(key string) (Object, error) { c.log.V(1).Info("fetching object", "key", key) obj, ok := c.objects[key] if !ok { return Object{}, fmt.Errorf("no object %s exists", key) } c.log.V(1).Info("pretending to deserialize object", "key", key, "json", "[insert real json here]") return obj, nil } // Save stores an object. func (c *Client) Save(obj Object) error { c.log.V(1).Info("saving object", "key", obj.Name, "object", obj) if rand.Intn(2) == 0 { return fmt.Errorf("couldn't save to %s", obj.Name) } c.log.V(1).Info("pretending to post object", "key", obj.Name, "url", "https://fake.test") return nil } // WatchNext waits for object updates. func (c *Client) WatchNext() string { time.Sleep(2 * time.Second) keyInd := rand.Intn(len(c.objects)) currInd := 0 for key := range c.objects { if currInd == keyInd { return key } currInd++ } c.log.Info("watch ended") return "" } // Controller is the main point of this example. type Controller struct { log logr.Logger expectedKind string client *Client } // Run starts the example controller. func (c *Controller) Run() { c.log.Info("starting reconciliation") for key := c.client.WatchNext(); key != ""; key = c.client.WatchNext() { // we can make more specific loggers if we always want to attach a particular named value log := c.log.WithValues("key", key) // fetch our object obj, err := c.client.Get(key) if err != nil { log.Error(err, "unable to reconcile object") continue } // make sure it's as expected if obj.Kind != c.expectedKind { log.Error(nil, "got object that wasn't expected kind", "actual-kind", obj.Kind, "object", obj) continue } // always log the object with log messages log = log.WithValues("object", obj) log.V(1).Info("reconciling object for key") // Do some complicated updates updates obj.Details = obj.Details.(int) * 2 // actually save the updates log.V(1).Info("updating object", "details", obj.Details) if err := c.client.Save(obj); err != nil { log.Error(err, "unable to reconcile object") } } c.log.Info("stopping reconciliation") } // NewController allocates and initializes a Controller. func NewController(log logr.Logger, objectKind string) *Controller { ctrlLogger := log.WithName("controller").WithName(objectKind) client := &Client{ log: ctrlLogger.WithName("client"), objects: objectMap, } return &Controller{ log: ctrlLogger, expectedKind: objectKind, client: client, } } func main() { // use a fake implementation just for demonstration purposes log := NewTabLogger() // update objects with the "one" kind ctrl := NewController(log, "one") ctrl.Run() } golang-github-go-logr-logr-1.4.2/funcr/000077500000000000000000000000001470525074300176765ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/funcr/example/000077500000000000000000000000001470525074300213315ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/funcr/example/main.go000066400000000000000000000030321470525074300226020ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package main is an example of using funcr. package main import ( "fmt" "github.com/go-logr/logr" "github.com/go-logr/logr/funcr" ) type e struct { str string } func (e e) Error() string { return e.str } func helper(log logr.Logger, msg string) { helper2(log, msg) } func helper2(log logr.Logger, msg string) { log.WithCallDepth(2).Info(msg) } func main() { // logr log := funcr.NewJSON( func(arg string) { fmt.Println(arg) }, funcr.Options{ LogCaller: funcr.All, LogTimestamp: true, Verbosity: 1, }) logrExample(log.WithName("logr").WithValues("mode", "funcr")) // slog (if possible) doSlog(log) } func logrExample(log logr.Logger) { log.Info("hello", "val1", 1, "val2", map[string]int{"k": 1}) log.V(1).Info("you should see this") log.V(1).V(1).Info("you should NOT see this") log.Error(nil, "uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) log.Error(e{"an error occurred"}, "goodbye", "code", -1) helper(log, "thru a helper") } golang-github-go-logr-logr-1.4.2/funcr/example/main_noslog.go000066400000000000000000000014451470525074300241710ustar00rootroot00000000000000//go:build !go1.21 // +build !go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package main is an example of using funcr. package main import ( "github.com/go-logr/logr" ) func doSlog(log logr.Logger) { log.Error(nil, "Sorry, slog is not supported on this version of Go") } golang-github-go-logr-logr-1.4.2/funcr/example/main_slog.go000066400000000000000000000024601470525074300236320ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package main is an example of using funcr. package main import ( "log/slog" "github.com/go-logr/logr" ) func doSlog(log logr.Logger) { slogger := slog.New(logr.ToSlogHandler(log.WithName("slog").WithValues("mode", "slog"))) slogExample(slogger) } func slogExample(log *slog.Logger) { log.Warn("hello", "val1", 1, "val2", map[string]int{"k": 1}) log.Info("you should see this") log.Debug("you should NOT see this") log.Error("uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) log.With("attr1", 1, "attr2", 2).Info("with attrs") log.WithGroup("groupname").Info("with group", "slog2", false) log.WithGroup("group1").With("attr1", 1).WithGroup("group2").With("attr2", 2).Info("msg", "arg", "val") } golang-github-go-logr-logr-1.4.2/funcr/example_formatter_test.go000066400000000000000000000052211470525074300250020ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package funcr_test import ( "fmt" "strings" "github.com/go-logr/logr" "github.com/go-logr/logr/funcr" ) // NewStdoutLogger returns a logr.Logger that prints to stdout. // It demonstrates how to implement a custom With* function which // controls whether INFO or ERROR are printed in front of the log // message. func NewStdoutLogger() logr.Logger { l := &stdoutlogger{ Formatter: funcr.NewFormatter(funcr.Options{}), } return logr.New(l) } type stdoutlogger struct { funcr.Formatter logMsgType bool } func (l stdoutlogger) WithName(name string) logr.LogSink { l.Formatter.AddName(name) return &l } func (l stdoutlogger) WithValues(kvList ...any) logr.LogSink { l.Formatter.AddValues(kvList) return &l } func (l stdoutlogger) WithCallDepth(depth int) logr.LogSink { l.Formatter.AddCallDepth(depth) return &l } func (l stdoutlogger) Info(level int, msg string, kvList ...any) { prefix, args := l.FormatInfo(level, msg, kvList) l.write("INFO", prefix, args) } func (l stdoutlogger) Error(err error, msg string, kvList ...any) { prefix, args := l.FormatError(err, msg, kvList) l.write("ERROR", prefix, args) } func (l stdoutlogger) write(msgType, prefix, args string) { var parts []string if l.logMsgType { parts = append(parts, msgType) } if prefix != "" { parts = append(parts, prefix) } parts = append(parts, args) fmt.Println(strings.Join(parts, ": ")) } // WithLogMsgType returns a copy of the logger with new settings for // logging the message type. It returns the original logger if the // underlying LogSink is not a stdoutlogger. func WithLogMsgType(log logr.Logger, logMsgType bool) logr.Logger { if l, ok := log.GetSink().(*stdoutlogger); ok { clone := *l clone.logMsgType = logMsgType log = log.WithSink(&clone) } return log } // Assert conformance to the interfaces. var _ logr.LogSink = &stdoutlogger{} var _ logr.CallDepthLogSink = &stdoutlogger{} func ExampleFormatter() { l := NewStdoutLogger() l.Info("no message type") WithLogMsgType(l, true).Info("with message type") // Output: // "level"=0 "msg"="no message type" // INFO: "level"=0 "msg"="with message type" } golang-github-go-logr-logr-1.4.2/funcr/example_test.go000066400000000000000000000073751470525074300227330ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package funcr_test import ( "fmt" "github.com/go-logr/logr" "github.com/go-logr/logr/funcr" ) func ExampleNew() { var log logr.Logger = funcr.New(func(prefix, args string) { fmt.Println(prefix, args) }, funcr.Options{}) log = log.WithName("MyLogger") log = log.WithValues("savedKey", "savedValue") log.Info("the message", "key", "value") // Output: MyLogger "level"=0 "msg"="the message" "savedKey"="savedValue" "key"="value" } func ExampleNewJSON() { var log logr.Logger = funcr.NewJSON(func(obj string) { fmt.Println(obj) }, funcr.Options{}) log = log.WithName("MyLogger") log = log.WithValues("savedKey", "savedValue") log.Info("the message", "key", "value") // Output: {"logger":"MyLogger","level":0,"msg":"the message","savedKey":"savedValue","key":"value"} } func ExampleUnderlier() { var log logr.Logger = funcr.New(func(prefix, args string) { fmt.Println(prefix, args) }, funcr.Options{}) if underlier, ok := log.GetSink().(funcr.Underlier); ok { fn := underlier.GetUnderlying() fn("hello", "world") } // Output: hello world } func ExampleOptions() { var log logr.Logger = funcr.NewJSON( func(obj string) { fmt.Println(obj) }, funcr.Options{ LogCaller: funcr.All, Verbosity: 1, // V(2) and higher is ignored. }) log.V(0).Info("V(0) message", "key", "value") log.V(1).Info("V(1) message", "key", "value") log.V(2).Info("V(2) message", "key", "value") // Output: // {"logger":"","caller":{"file":"example_test.go","line":67},"level":0,"msg":"V(0) message","key":"value"} // {"logger":"","caller":{"file":"example_test.go","line":68},"level":1,"msg":"V(1) message","key":"value"} } func ExampleOptions_renderHooks() { // prefix all builtin keys with "log:" prefixSpecialKeys := func(kvList []any) []any { for i := 0; i < len(kvList); i += 2 { k, _ := kvList[i].(string) kvList[i] = "log:" + k } return kvList } // present saved values as a single JSON object valuesAsObject := func(kvList []any) []any { return []any{"labels", funcr.PseudoStruct(kvList)} } var log logr.Logger = funcr.NewJSON( func(obj string) { fmt.Println(obj) }, funcr.Options{ RenderBuiltinsHook: prefixSpecialKeys, RenderValuesHook: valuesAsObject, }) log = log.WithName("MyLogger") log = log.WithValues("savedKey1", "savedVal1") log = log.WithValues("savedKey2", "savedVal2") log.Info("the message", "key", "value") // Output: {"log:logger":"MyLogger","log:level":0,"log:msg":"the message","labels":{"savedKey1":"savedVal1","savedKey2":"savedVal2"},"key":"value"} } func ExamplePseudoStruct() { var log logr.Logger = funcr.NewJSON( func(obj string) { fmt.Println(obj) }, funcr.Options{}) kv := []any{ "field1", 12345, "field2", true, } log.Info("the message", "key", funcr.PseudoStruct(kv)) // Output: {"logger":"","level":0,"msg":"the message","key":{"field1":12345,"field2":true}} } func ExampleOptions_maxLogDepth() { type List struct { Next *List } l := List{} l.Next = &l // recursive var log logr.Logger = funcr.NewJSON( func(obj string) { fmt.Println(obj) }, funcr.Options{MaxLogDepth: 4}) log.Info("recursive", "list", l) // Output: {"logger":"","level":0,"msg":"recursive","list":{"Next":{"Next":{"Next":{"Next":{"Next":""}}}}}} } golang-github-go-logr-logr-1.4.2/funcr/funcr.go000066400000000000000000000645501470525074300213540ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package funcr implements formatting of structured log messages and // optionally captures the call site and timestamp. // // The simplest way to use it is via its implementation of a // github.com/go-logr/logr.LogSink with output through an arbitrary // "write" function. See New and NewJSON for details. // // # Custom LogSinks // // For users who need more control, a funcr.Formatter can be embedded inside // your own custom LogSink implementation. This is useful when the LogSink // needs to implement additional methods, for example. // // # Formatting // // This will respect logr.Marshaler, fmt.Stringer, and error interfaces for // values which are being logged. When rendering a struct, funcr will use Go's // standard JSON tags (all except "string"). package funcr import ( "bytes" "encoding" "encoding/json" "fmt" "path/filepath" "reflect" "runtime" "strconv" "strings" "time" "github.com/go-logr/logr" ) // New returns a logr.Logger which is implemented by an arbitrary function. func New(fn func(prefix, args string), opts Options) logr.Logger { return logr.New(newSink(fn, NewFormatter(opts))) } // NewJSON returns a logr.Logger which is implemented by an arbitrary function // and produces JSON output. func NewJSON(fn func(obj string), opts Options) logr.Logger { fnWrapper := func(_, obj string) { fn(obj) } return logr.New(newSink(fnWrapper, NewFormatterJSON(opts))) } // Underlier exposes access to the underlying logging function. Since // callers only have a logr.Logger, they have to know which // implementation is in use, so this interface is less of an // abstraction and more of a way to test type conversion. type Underlier interface { GetUnderlying() func(prefix, args string) } func newSink(fn func(prefix, args string), formatter Formatter) logr.LogSink { l := &fnlogger{ Formatter: formatter, write: fn, } // For skipping fnlogger.Info and fnlogger.Error. l.Formatter.AddCallDepth(1) return l } // Options carries parameters which influence the way logs are generated. type Options struct { // LogCaller tells funcr to add a "caller" key to some or all log lines. // This has some overhead, so some users might not want it. LogCaller MessageClass // LogCallerFunc tells funcr to also log the calling function name. This // has no effect if caller logging is not enabled (see Options.LogCaller). LogCallerFunc bool // LogTimestamp tells funcr to add a "ts" key to log lines. This has some // overhead, so some users might not want it. LogTimestamp bool // TimestampFormat tells funcr how to render timestamps when LogTimestamp // is enabled. If not specified, a default format will be used. For more // details, see docs for Go's time.Layout. TimestampFormat string // LogInfoLevel tells funcr what key to use to log the info level. // If not specified, the info level will be logged as "level". // If this is set to "", the info level will not be logged at all. LogInfoLevel *string // Verbosity tells funcr which V logs to produce. Higher values enable // more logs. Info logs at or below this level will be written, while logs // above this level will be discarded. Verbosity int // RenderBuiltinsHook allows users to mutate the list of key-value pairs // while a log line is being rendered. The kvList argument follows logr // conventions - each pair of slice elements is comprised of a string key // and an arbitrary value (verified and sanitized before calling this // hook). The value returned must follow the same conventions. This hook // can be used to audit or modify logged data. For example, you might want // to prefix all of funcr's built-in keys with some string. This hook is // only called for built-in (provided by funcr itself) key-value pairs. // Equivalent hooks are offered for key-value pairs saved via // logr.Logger.WithValues or Formatter.AddValues (see RenderValuesHook) and // for user-provided pairs (see RenderArgsHook). RenderBuiltinsHook func(kvList []any) []any // RenderValuesHook is the same as RenderBuiltinsHook, except that it is // only called for key-value pairs saved via logr.Logger.WithValues. See // RenderBuiltinsHook for more details. RenderValuesHook func(kvList []any) []any // RenderArgsHook is the same as RenderBuiltinsHook, except that it is only // called for key-value pairs passed directly to Info and Error. See // RenderBuiltinsHook for more details. RenderArgsHook func(kvList []any) []any // MaxLogDepth tells funcr how many levels of nested fields (e.g. a struct // that contains a struct, etc.) it may log. Every time it finds a struct, // slice, array, or map the depth is increased by one. When the maximum is // reached, the value will be converted to a string indicating that the max // depth has been exceeded. If this field is not specified, a default // value will be used. MaxLogDepth int } // MessageClass indicates which category or categories of messages to consider. type MessageClass int const ( // None ignores all message classes. None MessageClass = iota // All considers all message classes. All // Info only considers info messages. Info // Error only considers error messages. Error ) // fnlogger inherits some of its LogSink implementation from Formatter // and just needs to add some glue code. type fnlogger struct { Formatter write func(prefix, args string) } func (l fnlogger) WithName(name string) logr.LogSink { l.Formatter.AddName(name) return &l } func (l fnlogger) WithValues(kvList ...any) logr.LogSink { l.Formatter.AddValues(kvList) return &l } func (l fnlogger) WithCallDepth(depth int) logr.LogSink { l.Formatter.AddCallDepth(depth) return &l } func (l fnlogger) Info(level int, msg string, kvList ...any) { prefix, args := l.FormatInfo(level, msg, kvList) l.write(prefix, args) } func (l fnlogger) Error(err error, msg string, kvList ...any) { prefix, args := l.FormatError(err, msg, kvList) l.write(prefix, args) } func (l fnlogger) GetUnderlying() func(prefix, args string) { return l.write } // Assert conformance to the interfaces. var _ logr.LogSink = &fnlogger{} var _ logr.CallDepthLogSink = &fnlogger{} var _ Underlier = &fnlogger{} // NewFormatter constructs a Formatter which emits a JSON-like key=value format. func NewFormatter(opts Options) Formatter { return newFormatter(opts, outputKeyValue) } // NewFormatterJSON constructs a Formatter which emits strict JSON. func NewFormatterJSON(opts Options) Formatter { return newFormatter(opts, outputJSON) } // Defaults for Options. const defaultTimestampFormat = "2006-01-02 15:04:05.000000" const defaultMaxLogDepth = 16 func newFormatter(opts Options, outfmt outputFormat) Formatter { if opts.TimestampFormat == "" { opts.TimestampFormat = defaultTimestampFormat } if opts.MaxLogDepth == 0 { opts.MaxLogDepth = defaultMaxLogDepth } if opts.LogInfoLevel == nil { opts.LogInfoLevel = new(string) *opts.LogInfoLevel = "level" } f := Formatter{ outputFormat: outfmt, prefix: "", values: nil, depth: 0, opts: &opts, } return f } // Formatter is an opaque struct which can be embedded in a LogSink // implementation. It should be constructed with NewFormatter. Some of // its methods directly implement logr.LogSink. type Formatter struct { outputFormat outputFormat prefix string values []any valuesStr string depth int opts *Options groupName string // for slog groups groups []groupDef } // outputFormat indicates which outputFormat to use. type outputFormat int const ( // outputKeyValue emits a JSON-like key=value format, but not strict JSON. outputKeyValue outputFormat = iota // outputJSON emits strict JSON. outputJSON ) // groupDef represents a saved group. The values may be empty, but we don't // know if we need to render the group until the final record is rendered. type groupDef struct { name string values string } // PseudoStruct is a list of key-value pairs that gets logged as a struct. type PseudoStruct []any // render produces a log line, ready to use. func (f Formatter) render(builtins, args []any) string { // Empirically bytes.Buffer is faster than strings.Builder for this. buf := bytes.NewBuffer(make([]byte, 0, 1024)) if f.outputFormat == outputJSON { buf.WriteByte('{') // for the whole record } // Render builtins vals := builtins if hook := f.opts.RenderBuiltinsHook; hook != nil { vals = hook(f.sanitize(vals)) } f.flatten(buf, vals, false) // keys are ours, no need to escape continuing := len(builtins) > 0 // Turn the inner-most group into a string argsStr := func() string { buf := bytes.NewBuffer(make([]byte, 0, 1024)) vals = args if hook := f.opts.RenderArgsHook; hook != nil { vals = hook(f.sanitize(vals)) } f.flatten(buf, vals, true) // escape user-provided keys return buf.String() }() // Render the stack of groups from the inside out. bodyStr := f.renderGroup(f.groupName, f.valuesStr, argsStr) for i := len(f.groups) - 1; i >= 0; i-- { grp := &f.groups[i] if grp.values == "" && bodyStr == "" { // no contents, so we must elide the whole group continue } bodyStr = f.renderGroup(grp.name, grp.values, bodyStr) } if bodyStr != "" { if continuing { buf.WriteByte(f.comma()) } buf.WriteString(bodyStr) } if f.outputFormat == outputJSON { buf.WriteByte('}') // for the whole record } return buf.String() } // renderGroup returns a string representation of the named group with rendered // values and args. If the name is empty, this will return the values and args, // joined. If the name is not empty, this will return a single key-value pair, // where the value is a grouping of the values and args. If the values and // args are both empty, this will return an empty string, even if the name was // specified. func (f Formatter) renderGroup(name string, values string, args string) string { buf := bytes.NewBuffer(make([]byte, 0, 1024)) needClosingBrace := false if name != "" && (values != "" || args != "") { buf.WriteString(f.quoted(name, true)) // escape user-provided keys buf.WriteByte(f.colon()) buf.WriteByte('{') needClosingBrace = true } continuing := false if values != "" { buf.WriteString(values) continuing = true } if args != "" { if continuing { buf.WriteByte(f.comma()) } buf.WriteString(args) } if needClosingBrace { buf.WriteByte('}') } return buf.String() } // flatten renders a list of key-value pairs into a buffer. If escapeKeys is // true, the keys are assumed to have non-JSON-compatible characters in them // and must be evaluated for escapes. // // This function returns a potentially modified version of kvList, which // ensures that there is a value for every key (adding a value if needed) and // that each key is a string (substituting a key if needed). func (f Formatter) flatten(buf *bytes.Buffer, kvList []any, escapeKeys bool) []any { // This logic overlaps with sanitize() but saves one type-cast per key, // which can be measurable. if len(kvList)%2 != 0 { kvList = append(kvList, noValue) } copied := false for i := 0; i < len(kvList); i += 2 { k, ok := kvList[i].(string) if !ok { if !copied { newList := make([]any, len(kvList)) copy(newList, kvList) kvList = newList copied = true } k = f.nonStringKey(kvList[i]) kvList[i] = k } v := kvList[i+1] if i > 0 { if f.outputFormat == outputJSON { buf.WriteByte(f.comma()) } else { // In theory the format could be something we don't understand. In // practice, we control it, so it won't be. buf.WriteByte(' ') } } buf.WriteString(f.quoted(k, escapeKeys)) buf.WriteByte(f.colon()) buf.WriteString(f.pretty(v)) } return kvList } func (f Formatter) quoted(str string, escape bool) string { if escape { return prettyString(str) } // this is faster return `"` + str + `"` } func (f Formatter) comma() byte { if f.outputFormat == outputJSON { return ',' } return ' ' } func (f Formatter) colon() byte { if f.outputFormat == outputJSON { return ':' } return '=' } func (f Formatter) pretty(value any) string { return f.prettyWithFlags(value, 0, 0) } const ( flagRawStruct = 0x1 // do not print braces on structs ) // TODO: This is not fast. Most of the overhead goes here. func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string { if depth > f.opts.MaxLogDepth { return `""` } // Handle types that take full control of logging. if v, ok := value.(logr.Marshaler); ok { // Replace the value with what the type wants to get logged. // That then gets handled below via reflection. value = invokeMarshaler(v) } // Handle types that want to format themselves. switch v := value.(type) { case fmt.Stringer: value = invokeStringer(v) case error: value = invokeError(v) } // Handling the most common types without reflect is a small perf win. switch v := value.(type) { case bool: return strconv.FormatBool(v) case string: return prettyString(v) case int: return strconv.FormatInt(int64(v), 10) case int8: return strconv.FormatInt(int64(v), 10) case int16: return strconv.FormatInt(int64(v), 10) case int32: return strconv.FormatInt(int64(v), 10) case int64: return strconv.FormatInt(int64(v), 10) case uint: return strconv.FormatUint(uint64(v), 10) case uint8: return strconv.FormatUint(uint64(v), 10) case uint16: return strconv.FormatUint(uint64(v), 10) case uint32: return strconv.FormatUint(uint64(v), 10) case uint64: return strconv.FormatUint(v, 10) case uintptr: return strconv.FormatUint(uint64(v), 10) case float32: return strconv.FormatFloat(float64(v), 'f', -1, 32) case float64: return strconv.FormatFloat(v, 'f', -1, 64) case complex64: return `"` + strconv.FormatComplex(complex128(v), 'f', -1, 64) + `"` case complex128: return `"` + strconv.FormatComplex(v, 'f', -1, 128) + `"` case PseudoStruct: buf := bytes.NewBuffer(make([]byte, 0, 1024)) v = f.sanitize(v) if flags&flagRawStruct == 0 { buf.WriteByte('{') } for i := 0; i < len(v); i += 2 { if i > 0 { buf.WriteByte(f.comma()) } k, _ := v[i].(string) // sanitize() above means no need to check success // arbitrary keys might need escaping buf.WriteString(prettyString(k)) buf.WriteByte(f.colon()) buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1)) } if flags&flagRawStruct == 0 { buf.WriteByte('}') } return buf.String() } buf := bytes.NewBuffer(make([]byte, 0, 256)) t := reflect.TypeOf(value) if t == nil { return "null" } v := reflect.ValueOf(value) switch t.Kind() { case reflect.Bool: return strconv.FormatBool(v.Bool()) case reflect.String: return prettyString(v.String()) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(int64(v.Int()), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return strconv.FormatUint(uint64(v.Uint()), 10) case reflect.Float32: return strconv.FormatFloat(float64(v.Float()), 'f', -1, 32) case reflect.Float64: return strconv.FormatFloat(v.Float(), 'f', -1, 64) case reflect.Complex64: return `"` + strconv.FormatComplex(complex128(v.Complex()), 'f', -1, 64) + `"` case reflect.Complex128: return `"` + strconv.FormatComplex(v.Complex(), 'f', -1, 128) + `"` case reflect.Struct: if flags&flagRawStruct == 0 { buf.WriteByte('{') } printComma := false // testing i>0 is not enough because of JSON omitted fields for i := 0; i < t.NumField(); i++ { fld := t.Field(i) if fld.PkgPath != "" { // reflect says this field is only defined for non-exported fields. continue } if !v.Field(i).CanInterface() { // reflect isn't clear exactly what this means, but we can't use it. continue } name := "" omitempty := false if tag, found := fld.Tag.Lookup("json"); found { if tag == "-" { continue } if comma := strings.Index(tag, ","); comma != -1 { if n := tag[:comma]; n != "" { name = n } rest := tag[comma:] if strings.Contains(rest, ",omitempty,") || strings.HasSuffix(rest, ",omitempty") { omitempty = true } } else { name = tag } } if omitempty && isEmpty(v.Field(i)) { continue } if printComma { buf.WriteByte(f.comma()) } printComma = true // if we got here, we are rendering a field if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" { buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1)) continue } if name == "" { name = fld.Name } // field names can't contain characters which need escaping buf.WriteString(f.quoted(name, false)) buf.WriteByte(f.colon()) buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1)) } if flags&flagRawStruct == 0 { buf.WriteByte('}') } return buf.String() case reflect.Slice, reflect.Array: // If this is outputing as JSON make sure this isn't really a json.RawMessage. // If so just emit "as-is" and don't pretty it as that will just print // it as [X,Y,Z,...] which isn't terribly useful vs the string form you really want. if f.outputFormat == outputJSON { if rm, ok := value.(json.RawMessage); ok { // If it's empty make sure we emit an empty value as the array style would below. if len(rm) > 0 { buf.Write(rm) } else { buf.WriteString("null") } return buf.String() } } buf.WriteByte('[') for i := 0; i < v.Len(); i++ { if i > 0 { buf.WriteByte(f.comma()) } e := v.Index(i) buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1)) } buf.WriteByte(']') return buf.String() case reflect.Map: buf.WriteByte('{') // This does not sort the map keys, for best perf. it := v.MapRange() i := 0 for it.Next() { if i > 0 { buf.WriteByte(f.comma()) } // If a map key supports TextMarshaler, use it. keystr := "" if m, ok := it.Key().Interface().(encoding.TextMarshaler); ok { txt, err := m.MarshalText() if err != nil { keystr = fmt.Sprintf("", err.Error()) } else { keystr = string(txt) } keystr = prettyString(keystr) } else { // prettyWithFlags will produce already-escaped values keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1) if t.Key().Kind() != reflect.String { // JSON only does string keys. Unlike Go's standard JSON, we'll // convert just about anything to a string. keystr = prettyString(keystr) } } buf.WriteString(keystr) buf.WriteByte(f.colon()) buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1)) i++ } buf.WriteByte('}') return buf.String() case reflect.Ptr, reflect.Interface: if v.IsNil() { return "null" } return f.prettyWithFlags(v.Elem().Interface(), 0, depth) } return fmt.Sprintf(`""`, t.Kind().String()) } func prettyString(s string) string { // Avoid escaping (which does allocations) if we can. if needsEscape(s) { return strconv.Quote(s) } b := bytes.NewBuffer(make([]byte, 0, 1024)) b.WriteByte('"') b.WriteString(s) b.WriteByte('"') return b.String() } // needsEscape determines whether the input string needs to be escaped or not, // without doing any allocations. func needsEscape(s string) bool { for _, r := range s { if !strconv.IsPrint(r) || r == '\\' || r == '"' { return true } } return false } func isEmpty(v reflect.Value) bool { switch v.Kind() { case reflect.Array, reflect.Map, reflect.Slice, reflect.String: return v.Len() == 0 case reflect.Bool: return !v.Bool() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 case reflect.Complex64, reflect.Complex128: return v.Complex() == 0 case reflect.Interface, reflect.Ptr: return v.IsNil() } return false } func invokeMarshaler(m logr.Marshaler) (ret any) { defer func() { if r := recover(); r != nil { ret = fmt.Sprintf("", r) } }() return m.MarshalLog() } func invokeStringer(s fmt.Stringer) (ret string) { defer func() { if r := recover(); r != nil { ret = fmt.Sprintf("", r) } }() return s.String() } func invokeError(e error) (ret string) { defer func() { if r := recover(); r != nil { ret = fmt.Sprintf("", r) } }() return e.Error() } // Caller represents the original call site for a log line, after considering // logr.Logger.WithCallDepth and logr.Logger.WithCallStackHelper. The File and // Line fields will always be provided, while the Func field is optional. // Users can set the render hook fields in Options to examine logged key-value // pairs, one of which will be {"caller", Caller} if the Options.LogCaller // field is enabled for the given MessageClass. type Caller struct { // File is the basename of the file for this call site. File string `json:"file"` // Line is the line number in the file for this call site. Line int `json:"line"` // Func is the function name for this call site, or empty if // Options.LogCallerFunc is not enabled. Func string `json:"function,omitempty"` } func (f Formatter) caller() Caller { // +1 for this frame, +1 for Info/Error. pc, file, line, ok := runtime.Caller(f.depth + 2) if !ok { return Caller{"", 0, ""} } fn := "" if f.opts.LogCallerFunc { if fp := runtime.FuncForPC(pc); fp != nil { fn = fp.Name() } } return Caller{filepath.Base(file), line, fn} } const noValue = "" func (f Formatter) nonStringKey(v any) string { return fmt.Sprintf("", f.snippet(v)) } // snippet produces a short snippet string of an arbitrary value. func (f Formatter) snippet(v any) string { const snipLen = 16 snip := f.pretty(v) if len(snip) > snipLen { snip = snip[:snipLen] } return snip } // sanitize ensures that a list of key-value pairs has a value for every key // (adding a value if needed) and that each key is a string (substituting a key // if needed). func (f Formatter) sanitize(kvList []any) []any { if len(kvList)%2 != 0 { kvList = append(kvList, noValue) } for i := 0; i < len(kvList); i += 2 { _, ok := kvList[i].(string) if !ok { kvList[i] = f.nonStringKey(kvList[i]) } } return kvList } // startGroup opens a new group scope (basically a sub-struct), which locks all // the current saved values and starts them anew. This is needed to satisfy // slog. func (f *Formatter) startGroup(name string) { // Unnamed groups are just inlined. if name == "" { return } n := len(f.groups) f.groups = append(f.groups[:n:n], groupDef{f.groupName, f.valuesStr}) // Start collecting new values. f.groupName = name f.valuesStr = "" f.values = nil } // Init configures this Formatter from runtime info, such as the call depth // imposed by logr itself. // Note that this receiver is a pointer, so depth can be saved. func (f *Formatter) Init(info logr.RuntimeInfo) { f.depth += info.CallDepth } // Enabled checks whether an info message at the given level should be logged. func (f Formatter) Enabled(level int) bool { return level <= f.opts.Verbosity } // GetDepth returns the current depth of this Formatter. This is useful for // implementations which do their own caller attribution. func (f Formatter) GetDepth() int { return f.depth } // FormatInfo renders an Info log message into strings. The prefix will be // empty when no names were set (via AddNames), or when the output is // configured for JSON. func (f Formatter) FormatInfo(level int, msg string, kvList []any) (prefix, argsStr string) { args := make([]any, 0, 64) // using a constant here impacts perf prefix = f.prefix if f.outputFormat == outputJSON { args = append(args, "logger", prefix) prefix = "" } if f.opts.LogTimestamp { args = append(args, "ts", time.Now().Format(f.opts.TimestampFormat)) } if policy := f.opts.LogCaller; policy == All || policy == Info { args = append(args, "caller", f.caller()) } if key := *f.opts.LogInfoLevel; key != "" { args = append(args, key, level) } args = append(args, "msg", msg) return prefix, f.render(args, kvList) } // FormatError renders an Error log message into strings. The prefix will be // empty when no names were set (via AddNames), or when the output is // configured for JSON. func (f Formatter) FormatError(err error, msg string, kvList []any) (prefix, argsStr string) { args := make([]any, 0, 64) // using a constant here impacts perf prefix = f.prefix if f.outputFormat == outputJSON { args = append(args, "logger", prefix) prefix = "" } if f.opts.LogTimestamp { args = append(args, "ts", time.Now().Format(f.opts.TimestampFormat)) } if policy := f.opts.LogCaller; policy == All || policy == Error { args = append(args, "caller", f.caller()) } args = append(args, "msg", msg) var loggableErr any if err != nil { loggableErr = err.Error() } args = append(args, "error", loggableErr) return prefix, f.render(args, kvList) } // AddName appends the specified name. funcr uses '/' characters to separate // name elements. Callers should not pass '/' in the provided name string, but // this library does not actually enforce that. func (f *Formatter) AddName(name string) { if len(f.prefix) > 0 { f.prefix += "/" } f.prefix += name } // AddValues adds key-value pairs to the set of saved values to be logged with // each log line. func (f *Formatter) AddValues(kvList []any) { // Three slice args forces a copy. n := len(f.values) f.values = append(f.values[:n:n], kvList...) vals := f.values if hook := f.opts.RenderValuesHook; hook != nil { vals = hook(f.sanitize(vals)) } // Pre-render values, so we don't have to do it on each Info/Error call. buf := bytes.NewBuffer(make([]byte, 0, 1024)) f.flatten(buf, vals, true) // escape user-provided keys f.valuesStr = buf.String() } // AddCallDepth increases the number of stack-frames to skip when attributing // the log line to a file and line. func (f *Formatter) AddCallDepth(depth int) { f.depth += depth } golang-github-go-logr-logr-1.4.2/funcr/funcr_test.go000066400000000000000000001154431470525074300224110ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package funcr import ( "encoding/json" "fmt" "path/filepath" "reflect" "runtime" "testing" "github.com/go-logr/logr" ) // Will be handled via reflection instead of type assertions. type substr string func ptrint(i int) *int { return &i } func ptrstr(s string) *string { return &s } // point implements encoding.TextMarshaler and can be used as a map key. type point struct{ x, y int } func (p point) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("(%d, %d)", p.x, p.y)), nil } // pointErr implements encoding.TextMarshaler but returns an error. type pointErr struct{ x, y int } func (p pointErr) MarshalText() ([]byte, error) { return nil, fmt.Errorf("uh oh: %d, %d", p.x, p.y) } // Logging this should result in the MarshalLog() value. type Tmarshaler struct{ val string } func (t Tmarshaler) MarshalLog() any { return struct{ Inner string }{"I am a logr.Marshaler"} } func (t Tmarshaler) String() string { return "String(): you should not see this" } func (t Tmarshaler) Error() string { return "Error(): you should not see this" } // Logging this should result in a panic. type Tmarshalerpanic struct{ val string } func (t Tmarshalerpanic) MarshalLog() any { panic("Tmarshalerpanic") } // Logging this should result in the String() value. type Tstringer struct{ val string } func (t Tstringer) String() string { return "I am a fmt.Stringer" } func (t Tstringer) Error() string { return "Error(): you should not see this" } // Logging this should result in a panic. type Tstringerpanic struct{ val string } func (t Tstringerpanic) String() string { panic("Tstringerpanic") } // Logging this should result in the Error() value. type Terror struct{ val string } func (t Terror) Error() string { return "I am an error" } // Logging this should result in a panic. type Terrorpanic struct{ val string } func (t Terrorpanic) Error() string { panic("Terrorpanic") } type TjsontagsString struct { String0 string `json:"-"` // first field ignored String1 string `json:"string1"` // renamed String2 string `json:"-"` // ignored String3 string `json:"-,"` // named "-" String4 string `json:"string4,omitempty"` // renamed, ignore if empty String5 string `json:","` // no-op String6 string `json:",omitempty"` // ignore if empty } type TjsontagsBool struct { Bool0 bool `json:"-"` // first field ignored Bool1 bool `json:"bool1"` // renamed Bool2 bool `json:"-"` // ignored Bool3 bool `json:"-,"` // named "-" Bool4 bool `json:"bool4,omitempty"` // renamed, ignore if empty Bool5 bool `json:","` // no-op Bool6 bool `json:",omitempty"` // ignore if empty } type TjsontagsInt struct { Int0 int `json:"-"` // first field ignored Int1 int `json:"int1"` // renamed Int2 int `json:"-"` // ignored Int3 int `json:"-,"` // named "-" Int4 int `json:"int4,omitempty"` // renamed, ignore if empty Int5 int `json:","` // no-op Int6 int `json:",omitempty"` // ignore if empty } type TjsontagsUint struct { Uint0 int `json:"-"` // first field ignored Uint1 uint `json:"uint1"` // renamed Uint2 uint `json:"-"` // ignored Uint3 uint `json:"-,"` // named "-" Uint4 uint `json:"uint4,omitempty"` // renamed, ignore if empty Uint5 uint `json:","` // no-op Uint6 uint `json:",omitempty"` // ignore if empty } type TjsontagsFloat struct { Float0 float64 `json:"-"` // first field ignored Float1 float64 `json:"float1"` // renamed Float2 float64 `json:"-"` // ignored Float3 float64 `json:"-,"` // named "-" Float4 float64 `json:"float4,omitempty"` // renamed, ignore if empty Float5 float64 `json:","` // no-op Float6 float64 `json:",omitempty"` // ignore if empty } type TjsontagsComplex struct { Complex0 complex128 `json:"-"` // first field ignored Complex1 complex128 `json:"complex1"` // renamed Complex2 complex128 `json:"-"` // ignored Complex3 complex128 `json:"-,"` // named "-" Complex4 complex128 `json:"complex4,omitempty"` // renamed, ignore if empty Complex5 complex128 `json:","` // no-op Complex6 complex128 `json:",omitempty"` // ignore if empty } type TjsontagsPtr struct { Ptr0 *string `json:"-"` // first field ignored Ptr1 *string `json:"ptr1"` // renamed Ptr2 *string `json:"-"` // ignored Ptr3 *string `json:"-,"` // named "-" Ptr4 *string `json:"ptr4,omitempty"` // renamed, ignore if empty Ptr5 *string `json:","` // no-op Ptr6 *string `json:",omitempty"` // ignore if empty } type TjsontagsArray struct { Array0 [2]string `json:"-"` // first field ignored Array1 [2]string `json:"array1"` // renamed Array2 [2]string `json:"-"` // ignored Array3 [2]string `json:"-,"` // named "-" Array4 [2]string `json:"array4,omitempty"` // renamed, ignore if empty Array5 [2]string `json:","` // no-op Array6 [2]string `json:",omitempty"` // ignore if empty } type TjsontagsSlice struct { Slice0 []string `json:"-"` // first field ignored Slice1 []string `json:"slice1"` // renamed Slice2 []string `json:"-"` // ignored Slice3 []string `json:"-,"` // named "-" Slice4 []string `json:"slice4,omitempty"` // renamed, ignore if empty Slice5 []string `json:","` // no-op Slice6 []string `json:",omitempty"` // ignore if empty } type TjsontagsMap struct { Map0 map[string]string `json:"-"` // first field ignored Map1 map[string]string `json:"map1"` // renamed Map2 map[string]string `json:"-"` // ignored Map3 map[string]string `json:"-,"` // named "-" Map4 map[string]string `json:"map4,omitempty"` // renamed, ignore if empty Map5 map[string]string `json:","` // no-op Map6 map[string]string `json:",omitempty"` // ignore if empty } type Tinnerstruct struct { Inner string } type Tinnerint int type Tinnermap map[string]string type Tinnerslice []string type Tembedstruct struct { Tinnerstruct Outer string } type Tembednonstruct struct { Tinnerint Tinnermap Tinnerslice } type Tinner1 Tinnerstruct type Tinner2 Tinnerstruct type Tinner3 Tinnerstruct type Tinner4 Tinnerstruct type Tinner5 Tinnerstruct type Tinner6 Tinnerstruct type Tembedjsontags struct { Outer string Tinner1 `json:"inner1"` Tinner2 `json:"-"` Tinner3 `json:"-,"` Tinner4 `json:"inner4,omitempty"` Tinner5 `json:","` Tinner6 `json:"inner6,omitempty"` } type Trawjson struct { Message json.RawMessage `json:"message"` } func TestPretty(t *testing.T) { // used below newStr := func(s string) *string { return &s } cases := []struct { val any exp string // used in cases where JSON can't handle it }{{ val: "strval", }, { val: "strval\nwith\t\"escapes\"", }, { val: substr("substrval"), }, { val: substr("substrval\nwith\t\"escapes\""), }, { val: true, }, { val: false, }, { val: int(93), }, { val: int8(93), }, { val: int16(93), }, { val: int32(93), }, { val: int64(93), }, { val: int(-93), }, { val: int8(-93), }, { val: int16(-93), }, { val: int32(-93), }, { val: int64(-93), }, { val: uint(93), }, { val: uint8(93), }, { val: uint16(93), }, { val: uint32(93), }, { val: uint64(93), }, { val: uintptr(93), }, { val: float32(93.76), }, { val: float64(93.76), }, { val: complex64(93i), exp: `"(0+93i)"`, }, { val: complex128(93i), exp: `"(0+93i)"`, }, { val: ptrint(93), }, { val: ptrstr("pstrval"), }, { val: []int{}, }, { val: []int(nil), exp: `[]`, }, { val: []int{9, 3, 7, 6}, }, { val: []string{"str", "with\tescape"}, }, { val: []substr{"substr", "with\tescape"}, }, { val: [4]int{9, 3, 7, 6}, }, { val: [2]string{"str", "with\tescape"}, }, { val: [2]substr{"substr", "with\tescape"}, }, { val: struct { Int int notExported string String string }{ 93, "you should not see this", "seventy-six", }, }, { val: map[string]int{}, }, { val: map[string]int(nil), exp: `{}`, }, { val: map[string]int{ "nine": 3, }, }, { val: map[string]int{ "with\tescape": 76, }, }, { val: map[substr]int{ "nine": 3, }, }, { val: map[substr]int{ "with\tescape": 76, }, }, { val: map[int]int{ 9: 3, }, }, { val: map[float64]int{ 9.5: 3, }, exp: `{"9.5":3}`, }, { val: map[point]int{ {x: 1, y: 2}: 3, }, }, { val: map[pointErr]int{ {x: 1, y: 2}: 3, }, exp: `{"":3}`, }, { val: struct { X int `json:"x"` Y int `json:"y"` }{ 93, 76, }, }, { val: struct { X []int Y map[int]int Z struct{ P, Q int } }{ []int{9, 3, 7, 6}, map[int]int{9: 3}, struct{ P, Q int }{9, 3}, }, }, { val: []struct{ X, Y string }{ {"nine", "three"}, {"seven", "six"}, {"with\t", "\tescapes"}, }, }, { val: struct { A *int B *int C any D any }{ B: ptrint(1), D: any(2), }, }, { val: Tmarshaler{"foobar"}, exp: `{"Inner":"I am a logr.Marshaler"}`, }, { val: &Tmarshaler{"foobar"}, exp: `{"Inner":"I am a logr.Marshaler"}`, }, { val: (*Tmarshaler)(nil), exp: `""`, }, { val: Tmarshalerpanic{"foobar"}, exp: `""`, }, { val: Tstringer{"foobar"}, exp: `"I am a fmt.Stringer"`, }, { val: &Tstringer{"foobar"}, exp: `"I am a fmt.Stringer"`, }, { val: (*Tstringer)(nil), exp: `""`, }, { val: Tstringerpanic{"foobar"}, exp: `""`, }, { val: Terror{"foobar"}, exp: `"I am an error"`, }, { val: &Terror{"foobar"}, exp: `"I am an error"`, }, { val: (*Terror)(nil), exp: `""`, }, { val: Terrorpanic{"foobar"}, exp: `""`, }, { val: TjsontagsString{ String1: "v1", String2: "v2", String3: "v3", String4: "v4", String5: "v5", String6: "v6", }, }, { val: TjsontagsString{}, }, { val: TjsontagsBool{ Bool1: true, Bool2: true, Bool3: true, Bool4: true, Bool5: true, Bool6: true, }, }, { val: TjsontagsBool{}, }, { val: TjsontagsInt{ Int1: 1, Int2: 2, Int3: 3, Int4: 4, Int5: 5, Int6: 6, }, }, { val: TjsontagsInt{}, }, { val: TjsontagsUint{ Uint1: 1, Uint2: 2, Uint3: 3, Uint4: 4, Uint5: 5, Uint6: 6, }, }, { val: TjsontagsUint{}, }, { val: TjsontagsFloat{ Float1: 1.1, Float2: 2.2, Float3: 3.3, Float4: 4.4, Float5: 5.5, Float6: 6.6, }, }, { val: TjsontagsFloat{}, }, { val: TjsontagsComplex{ Complex1: 1i, Complex2: 2i, Complex3: 3i, Complex4: 4i, Complex5: 5i, Complex6: 6i, }, exp: `{"complex1":"(0+1i)","-":"(0+3i)","complex4":"(0+4i)","Complex5":"(0+5i)","Complex6":"(0+6i)"}`, }, { val: TjsontagsComplex{}, exp: `{"complex1":"(0+0i)","-":"(0+0i)","Complex5":"(0+0i)"}`, }, { val: TjsontagsPtr{ Ptr1: newStr("1"), Ptr2: newStr("2"), Ptr3: newStr("3"), Ptr4: newStr("4"), Ptr5: newStr("5"), Ptr6: newStr("6"), }, }, { val: TjsontagsPtr{}, }, { val: TjsontagsArray{ Array1: [2]string{"v1", "v1"}, Array2: [2]string{"v2", "v2"}, Array3: [2]string{"v3", "v3"}, Array4: [2]string{"v4", "v4"}, Array5: [2]string{"v5", "v5"}, Array6: [2]string{"v6", "v6"}, }, }, { val: TjsontagsArray{}, }, { val: TjsontagsSlice{ Slice1: []string{"v1", "v1"}, Slice2: []string{"v2", "v2"}, Slice3: []string{"v3", "v3"}, Slice4: []string{"v4", "v4"}, Slice5: []string{"v5", "v5"}, Slice6: []string{"v6", "v6"}, }, }, { val: TjsontagsSlice{}, exp: `{"slice1":[],"-":[],"Slice5":[]}`, }, { val: TjsontagsMap{ Map1: map[string]string{"k1": "v1"}, Map2: map[string]string{"k2": "v2"}, Map3: map[string]string{"k3": "v3"}, Map4: map[string]string{"k4": "v4"}, Map5: map[string]string{"k5": "v5"}, Map6: map[string]string{"k6": "v6"}, }, }, { val: TjsontagsMap{}, exp: `{"map1":{},"-":{},"Map5":{}}`, }, { val: Tembedstruct{}, }, { val: Tembednonstruct{}, exp: `{"Tinnerint":0,"Tinnermap":{},"Tinnerslice":[]}`, }, { val: Tembedjsontags{}, }, { val: PseudoStruct(makeKV("f1", 1, "f2", true, "f3", []int{})), exp: `{"f1":1,"f2":true,"f3":[]}`, }, { val: map[TjsontagsString]int{ {String1: `"quoted"`, String4: `unquoted`}: 1, }, exp: `{"{\"string1\":\"\\\"quoted\\\"\",\"-\":\"\",\"string4\":\"unquoted\",\"String5\":\"\"}":1}`, }, { val: map[TjsontagsInt]int{ {Int1: 1, Int2: 2}: 3, }, exp: `{"{\"int1\":1,\"-\":0,\"Int5\":0}":3}`, }, { val: map[[2]struct{ S string }]int{ {{S: `"quoted"`}, {S: "unquoted"}}: 1, }, exp: `{"[{\"S\":\"\\\"quoted\\\"\"},{\"S\":\"unquoted\"}]":1}`, }, { val: TjsontagsComplex{}, exp: `{"complex1":"(0+0i)","-":"(0+0i)","Complex5":"(0+0i)"}`, }, { val: TjsontagsPtr{ Ptr1: newStr("1"), Ptr2: newStr("2"), Ptr3: newStr("3"), Ptr4: newStr("4"), Ptr5: newStr("5"), Ptr6: newStr("6"), }, }, { val: TjsontagsPtr{}, }, { val: TjsontagsArray{ Array1: [2]string{"v1", "v1"}, Array2: [2]string{"v2", "v2"}, Array3: [2]string{"v3", "v3"}, Array4: [2]string{"v4", "v4"}, Array5: [2]string{"v5", "v5"}, Array6: [2]string{"v6", "v6"}, }, }, { val: TjsontagsArray{}, }, { val: TjsontagsSlice{ Slice1: []string{"v1", "v1"}, Slice2: []string{"v2", "v2"}, Slice3: []string{"v3", "v3"}, Slice4: []string{"v4", "v4"}, Slice5: []string{"v5", "v5"}, Slice6: []string{"v6", "v6"}, }, }, { val: TjsontagsSlice{}, exp: `{"slice1":[],"-":[],"Slice5":[]}`, }, { val: TjsontagsMap{ Map1: map[string]string{"k1": "v1"}, Map2: map[string]string{"k2": "v2"}, Map3: map[string]string{"k3": "v3"}, Map4: map[string]string{"k4": "v4"}, Map5: map[string]string{"k5": "v5"}, Map6: map[string]string{"k6": "v6"}, }, }, { val: TjsontagsMap{}, exp: `{"map1":{},"-":{},"Map5":{}}`, }, { val: Tembedstruct{}, }, { val: Tembednonstruct{}, exp: `{"Tinnerint":0,"Tinnermap":{},"Tinnerslice":[]}`, }, { val: Tembedjsontags{}, }, { val: PseudoStruct(makeKV("f1", 1, "f2", true, "f3", []int{})), exp: `{"f1":1,"f2":true,"f3":[]}`, }, { val: map[TjsontagsString]int{ {String1: `"quoted"`, String4: `unquoted`}: 1, }, exp: `{"{\"string1\":\"\\\"quoted\\\"\",\"-\":\"\",\"string4\":\"unquoted\",\"String5\":\"\"}":1}`, }, { val: map[TjsontagsInt]int{ {Int1: 1, Int2: 2}: 3, }, exp: `{"{\"int1\":1,\"-\":0,\"Int5\":0}":3}`, }, { val: map[[2]struct{ S string }]int{ {{S: `"quoted"`}, {S: "unquoted"}}: 1, }, exp: `{"[{\"S\":\"\\\"quoted\\\"\"},{\"S\":\"unquoted\"}]":1}`, }} f := NewFormatterJSON(Options{}) for i, tc := range cases { ours := f.pretty(tc.val) want := "" if tc.exp != "" { want = tc.exp } else { jb, err := json.Marshal(tc.val) if err != nil { t.Fatalf("[%d]: unexpected error: %v\ngot: %q", i, err, ours) } want = string(jb) } if ours != want { t.Errorf("[%d]:\n\texpected %q\n\tgot %q", i, want, ours) } } } func makeKV(args ...any) []any { return args } func TestRender(t *testing.T) { // used below raw := &Trawjson{} marshal := &TjsontagsInt{} var err error raw.Message, err = json.Marshal(marshal) if err != nil { t.Fatalf("json.Marshal error: %v", err) } testCases := []struct { name string builtins []any values []any args []any expectKV string expectJSON string }{{ name: "nil", expectKV: "", expectJSON: "{}", }, { name: "empty", builtins: []any{}, values: []any{}, args: []any{}, expectKV: "", expectJSON: "{}", }, { name: "primitives", builtins: makeKV("int1", 1, "int2", 2), values: makeKV("str1", "ABC", "str2", "DEF"), args: makeKV("bool1", true, "bool2", false), expectKV: `"int1"=1 "int2"=2 "str1"="ABC" "str2"="DEF" "bool1"=true "bool2"=false`, expectJSON: `{"int1":1,"int2":2,"str1":"ABC","str2":"DEF","bool1":true,"bool2":false}`, }, { name: "pseudo structs", builtins: makeKV("int", PseudoStruct(makeKV("intsub", 1))), values: makeKV("str", PseudoStruct(makeKV("strsub", "2"))), args: makeKV("bool", PseudoStruct(makeKV("boolsub", true))), expectKV: `"int"={"intsub"=1} "str"={"strsub"="2"} "bool"={"boolsub"=true}`, expectJSON: `{"int":{"intsub":1},"str":{"strsub":"2"},"bool":{"boolsub":true}}`, }, { name: "escapes", builtins: makeKV("\"1\"", 1), // will not be escaped, but should never happen values: makeKV("\tstr", "ABC"), // escaped args: makeKV("bool\n", true), // escaped expectKV: `""1""=1 "\tstr"="ABC" "bool\n"=true`, expectJSON: `{""1"":1,"\tstr":"ABC","bool\n":true}`, }, { name: "missing value", builtins: makeKV("builtin"), values: makeKV("value"), args: makeKV("arg"), expectKV: `"builtin"="" "value"="" "arg"=""`, expectJSON: `{"builtin":"","value":"","arg":""}`, }, { name: "non-string key int", builtins: makeKV(123, "val"), // should never happen values: makeKV(456, "val"), args: makeKV(789, "val"), expectKV: `""="val" ""="val" ""="val"`, expectJSON: `{"":"val","":"val","":"val"}`, }, { name: "non-string key struct", builtins: makeKV(struct { // will not be escaped, but should never happen F1 string F2 int }{"builtin", 123}, "val"), values: makeKV(struct { F1 string F2 int }{"value", 456}, "val"), args: makeKV(struct { F1 string F2 int }{"arg", 789}, "val"), expectKV: `""="val" ""="val" ""="val"`, expectJSON: `{"":"val","":"val","":"val"}`, }, { name: "json rendering with json.RawMessage", args: makeKV("key", raw), expectKV: `"key"={"message"=[123 34 105 110 116 49 34 58 48 44 34 45 34 58 48 44 34 73 110 116 53 34 58 48 125]}`, expectJSON: `{"key":{"message":{"int1":0,"-":0,"Int5":0}}}`, }, { name: "byte array not json.RawMessage", args: makeKV("key", []byte{1, 2, 3, 4}), expectKV: `"key"=[1 2 3 4]`, expectJSON: `{"key":[1,2,3,4]}`, }, { name: "json rendering with empty json.RawMessage", args: makeKV("key", &Trawjson{}), expectKV: `"key"={"message"=[]}`, expectJSON: `{"key":{"message":null}}`, }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { test := func(t *testing.T, formatter Formatter, expect string) { formatter.AddValues(tc.values) r := formatter.render(tc.builtins, tc.args) if r != expect { t.Errorf("wrong output:\nexpected %q\n got %q", expect, r) } } t.Run("KV", func(t *testing.T) { test(t, NewFormatter(Options{}), tc.expectKV) }) t.Run("JSON", func(t *testing.T) { test(t, NewFormatterJSON(Options{}), tc.expectJSON) }) }) } } func TestSanitize(t *testing.T) { testCases := []struct { name string kv []any expect []any }{{ name: "empty", kv: []any{}, expect: []any{}, }, { name: "already sane", kv: makeKV("int", 1, "str", "ABC", "bool", true), expect: makeKV("int", 1, "str", "ABC", "bool", true), }, { name: "missing value", kv: makeKV("key"), expect: makeKV("key", ""), }, { name: "non-string key int", kv: makeKV(123, "val"), expect: makeKV("", "val"), }, { name: "non-string key struct", kv: makeKV(struct { F1 string F2 int }{"f1", 8675309}, "val"), expect: makeKV(``, "val"), }} f := NewFormatterJSON(Options{}) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { r := f.sanitize(tc.kv) if !reflect.DeepEqual(r, tc.expect) { t.Errorf("wrong output:\nexpected %q\n got %q", tc.expect, r) } }) } } func TestEnabled(t *testing.T) { t.Run("default V", func(t *testing.T) { log := newSink(func(_, _ string) {}, NewFormatter(Options{})) if !log.Enabled(0) { t.Errorf("expected true") } if log.Enabled(1) { t.Errorf("expected false") } }) t.Run("V=9", func(t *testing.T) { log := newSink(func(_, _ string) {}, NewFormatter(Options{Verbosity: 9})) if !log.Enabled(8) { t.Errorf("expected true") } if !log.Enabled(9) { t.Errorf("expected true") } if log.Enabled(10) { t.Errorf("expected false") } }) } type capture struct { log string } func (c *capture) Func(prefix, args string) { space := " " if len(prefix) == 0 { space = "" } c.log = prefix + space + args } func TestInfo(t *testing.T) { testCases := []struct { name string args []any expectKV string expectJSON string }{{ name: "just msg", args: makeKV(), expectKV: `"level"=0 "msg"="msg"`, expectJSON: `{"logger":"","level":0,"msg":"msg"}`, }, { name: "primitives", args: makeKV("int", 1, "str", "ABC", "bool", true), expectKV: `"level"=0 "msg"="msg" "int"=1 "str"="ABC" "bool"=true`, expectJSON: `{"logger":"","level":0,"msg":"msg","int":1,"str":"ABC","bool":true}`, }} for _, tc := range testCases { t.Run("KV: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{})) sink.Info(0, "msg", tc.args...) if capt.log != tc.expectKV { t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) } }) t.Run("JSON: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatterJSON(Options{})) sink.Info(0, "msg", tc.args...) if capt.log != tc.expectJSON { t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) } }) } } func TestInfoWithCaller(t *testing.T) { t.Run("KV: LogCaller=All", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: All})) sink.Info(0, "msg") _, file, line, _ := runtime.Caller(0) expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "level"=0 "msg"="msg"`, filepath.Base(file), line-1) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } sink.Error(fmt.Errorf("error"), "msg") _, file, line, _ = runtime.Caller(0) expect = fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "msg"="msg" "error"="error"`, filepath.Base(file), line-1) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) t.Run("JSON: LogCaller=All", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatterJSON(Options{LogCaller: All})) sink.Info(0, "msg") _, file, line, _ := runtime.Caller(0) expect := fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d},"level":0,"msg":"msg"}`, filepath.Base(file), line-1) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } sink.Error(fmt.Errorf("error"), "msg") _, file, line, _ = runtime.Caller(0) expect = fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d},"msg":"msg","error":"error"}`, filepath.Base(file), line-1) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) t.Run("KV: LogCaller=All, LogCallerFunc=true", func(t *testing.T) { thisFunc := "github.com/go-logr/logr/funcr.TestInfoWithCaller.func3" capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: All, LogCallerFunc: true})) sink.Info(0, "msg") _, file, line, _ := runtime.Caller(0) expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d "function"=%q} "level"=0 "msg"="msg"`, filepath.Base(file), line-1, thisFunc) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } sink.Error(fmt.Errorf("error"), "msg") _, file, line, _ = runtime.Caller(0) expect = fmt.Sprintf(`"caller"={"file"=%q "line"=%d "function"=%q} "msg"="msg" "error"="error"`, filepath.Base(file), line-1, thisFunc) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) t.Run("JSON: LogCaller=All, LogCallerFunc=true", func(t *testing.T) { thisFunc := "github.com/go-logr/logr/funcr.TestInfoWithCaller.func4" capt := &capture{} sink := newSink(capt.Func, NewFormatterJSON(Options{LogCaller: All, LogCallerFunc: true})) sink.Info(0, "msg") _, file, line, _ := runtime.Caller(0) expect := fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d,"function":%q},"level":0,"msg":"msg"}`, filepath.Base(file), line-1, thisFunc) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } sink.Error(fmt.Errorf("error"), "msg") _, file, line, _ = runtime.Caller(0) expect = fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d,"function":%q},"msg":"msg","error":"error"}`, filepath.Base(file), line-1, thisFunc) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) t.Run("LogCaller=Info", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: Info})) sink.Info(0, "msg") _, file, line, _ := runtime.Caller(0) expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "level"=0 "msg"="msg"`, filepath.Base(file), line-1) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } sink.Error(fmt.Errorf("error"), "msg") expect = `"msg"="msg" "error"="error"` if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) t.Run("LogCaller=Error", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: Error})) sink.Info(0, "msg") expect := `"level"=0 "msg"="msg"` if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } sink.Error(fmt.Errorf("error"), "msg") _, file, line, _ := runtime.Caller(0) expect = fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "msg"="msg" "error"="error"`, filepath.Base(file), line-1) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) t.Run("LogCaller=None", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: None})) sink.Info(0, "msg") expect := `"level"=0 "msg"="msg"` if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } sink.Error(fmt.Errorf("error"), "msg") expect = `"msg"="msg" "error"="error"` if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) } func TestError(t *testing.T) { testCases := []struct { name string args []any expectKV string expectJSON string }{{ name: "just msg", args: makeKV(), expectKV: `"msg"="msg" "error"="err"`, expectJSON: `{"logger":"","msg":"msg","error":"err"}`, }, { name: "primitives", args: makeKV("int", 1, "str", "ABC", "bool", true), expectKV: `"msg"="msg" "error"="err" "int"=1 "str"="ABC" "bool"=true`, expectJSON: `{"logger":"","msg":"msg","error":"err","int":1,"str":"ABC","bool":true}`, }} for _, tc := range testCases { t.Run("KV: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{})) sink.Error(fmt.Errorf("err"), "msg", tc.args...) if capt.log != tc.expectKV { t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) } }) t.Run("JSON: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatterJSON(Options{})) sink.Error(fmt.Errorf("err"), "msg", tc.args...) if capt.log != tc.expectJSON { t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) } }) } } func TestErrorWithCaller(t *testing.T) { t.Run("KV: LogCaller=All", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: All})) sink.Error(fmt.Errorf("err"), "msg") _, file, line, _ := runtime.Caller(0) expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "msg"="msg" "error"="err"`, filepath.Base(file), line-1) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) t.Run("JSON: LogCaller=All", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatterJSON(Options{LogCaller: All})) sink.Error(fmt.Errorf("err"), "msg") _, file, line, _ := runtime.Caller(0) expect := fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d},"msg":"msg","error":"err"}`, filepath.Base(file), line-1) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) t.Run("LogCaller=Error", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: Error})) sink.Error(fmt.Errorf("err"), "msg") _, file, line, _ := runtime.Caller(0) expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "msg"="msg" "error"="err"`, filepath.Base(file), line-1) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) t.Run("LogCaller=Info", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: Info})) sink.Error(fmt.Errorf("err"), "msg") expect := `"msg"="msg" "error"="err"` if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) t.Run("LogCaller=None", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: None})) sink.Error(fmt.Errorf("err"), "msg") expect := `"msg"="msg" "error"="err"` if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) } func TestInfoWithName(t *testing.T) { testCases := []struct { name string names []string args []any expectKV string expectJSON string }{{ name: "one", names: []string{"pfx1"}, args: makeKV("k", "v"), expectKV: `pfx1 "level"=0 "msg"="msg" "k"="v"`, expectJSON: `{"logger":"pfx1","level":0,"msg":"msg","k":"v"}`, }, { name: "two", names: []string{"pfx1", "pfx2"}, args: makeKV("k", "v"), expectKV: `pfx1/pfx2 "level"=0 "msg"="msg" "k"="v"`, expectJSON: `{"logger":"pfx1/pfx2","level":0,"msg":"msg","k":"v"}`, }} for _, tc := range testCases { t.Run("KV: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{})) for _, n := range tc.names { sink = sink.WithName(n) } sink.Info(0, "msg", tc.args...) if capt.log != tc.expectKV { t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) } }) t.Run("JSON: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatterJSON(Options{})) for _, n := range tc.names { sink = sink.WithName(n) } sink.Info(0, "msg", tc.args...) if capt.log != tc.expectJSON { t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) } }) } } func TestErrorWithName(t *testing.T) { testCases := []struct { name string names []string args []any expectKV string expectJSON string }{{ name: "one", names: []string{"pfx1"}, args: makeKV("k", "v"), expectKV: `pfx1 "msg"="msg" "error"="err" "k"="v"`, expectJSON: `{"logger":"pfx1","msg":"msg","error":"err","k":"v"}`, }, { name: "two", names: []string{"pfx1", "pfx2"}, args: makeKV("k", "v"), expectKV: `pfx1/pfx2 "msg"="msg" "error"="err" "k"="v"`, expectJSON: `{"logger":"pfx1/pfx2","msg":"msg","error":"err","k":"v"}`, }} for _, tc := range testCases { t.Run("KV: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{})) for _, n := range tc.names { sink = sink.WithName(n) } sink.Error(fmt.Errorf("err"), "msg", tc.args...) if capt.log != tc.expectKV { t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) } }) t.Run("JSON: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatterJSON(Options{})) for _, n := range tc.names { sink = sink.WithName(n) } sink.Error(fmt.Errorf("err"), "msg", tc.args...) if capt.log != tc.expectJSON { t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) } }) } } func TestInfoWithValues(t *testing.T) { testCases := []struct { name string values []any args []any expectKV string expectJSON string }{{ name: "zero", values: makeKV(), args: makeKV("k", "v"), expectKV: `"level"=0 "msg"="msg" "k"="v"`, expectJSON: `{"logger":"","level":0,"msg":"msg","k":"v"}`, }, { name: "one", values: makeKV("one", 1), args: makeKV("k", "v"), expectKV: `"level"=0 "msg"="msg" "one"=1 "k"="v"`, expectJSON: `{"logger":"","level":0,"msg":"msg","one":1,"k":"v"}`, }, { name: "two", values: makeKV("one", 1, "two", 2), args: makeKV("k", "v"), expectKV: `"level"=0 "msg"="msg" "one"=1 "two"=2 "k"="v"`, expectJSON: `{"logger":"","level":0,"msg":"msg","one":1,"two":2,"k":"v"}`, }, { name: "dangling", values: makeKV("dangling"), args: makeKV("k", "v"), expectKV: `"level"=0 "msg"="msg" "dangling"="" "k"="v"`, expectJSON: `{"logger":"","level":0,"msg":"msg","dangling":"","k":"v"}`, }} for _, tc := range testCases { t.Run("KV: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{})) sink = sink.WithValues(tc.values...) sink.Info(0, "msg", tc.args...) if capt.log != tc.expectKV { t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) } }) t.Run("JSON: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatterJSON(Options{})) sink = sink.WithValues(tc.values...) sink.Info(0, "msg", tc.args...) if capt.log != tc.expectJSON { t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) } }) } } func TestErrorWithValues(t *testing.T) { testCases := []struct { name string values []any args []any expectKV string expectJSON string }{{ name: "zero", values: makeKV(), args: makeKV("k", "v"), expectKV: `"msg"="msg" "error"="err" "k"="v"`, expectJSON: `{"logger":"","msg":"msg","error":"err","k":"v"}`, }, { name: "one", values: makeKV("one", 1), args: makeKV("k", "v"), expectKV: `"msg"="msg" "error"="err" "one"=1 "k"="v"`, expectJSON: `{"logger":"","msg":"msg","error":"err","one":1,"k":"v"}`, }, { name: "two", values: makeKV("one", 1, "two", 2), args: makeKV("k", "v"), expectKV: `"msg"="msg" "error"="err" "one"=1 "two"=2 "k"="v"`, expectJSON: `{"logger":"","msg":"msg","error":"err","one":1,"two":2,"k":"v"}`, }, { name: "dangling", values: makeKV("dangling"), args: makeKV("k", "v"), expectKV: `"msg"="msg" "error"="err" "dangling"="" "k"="v"`, expectJSON: `{"logger":"","msg":"msg","error":"err","dangling":"","k":"v"}`, }} for _, tc := range testCases { t.Run("KV: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{})) sink = sink.WithValues(tc.values...) sink.Error(fmt.Errorf("err"), "msg", tc.args...) if capt.log != tc.expectKV { t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) } }) t.Run("JSON: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatterJSON(Options{})) sink = sink.WithValues(tc.values...) sink.Error(fmt.Errorf("err"), "msg", tc.args...) if capt.log != tc.expectJSON { t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) } }) } } func TestInfoWithCallDepth(t *testing.T) { t.Run("one", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: All})) dSink, _ := sink.(logr.CallDepthLogSink) sink = dSink.WithCallDepth(1) sink.Info(0, "msg") _, file, line, _ := runtime.Caller(1) expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "level"=0 "msg"="msg"`, filepath.Base(file), line) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) } func TestErrorWithCallDepth(t *testing.T) { t.Run("one", func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogCaller: All})) dSink, _ := sink.(logr.CallDepthLogSink) sink = dSink.WithCallDepth(1) sink.Error(fmt.Errorf("err"), "msg") _, file, line, _ := runtime.Caller(1) expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "msg"="msg" "error"="err"`, filepath.Base(file), line) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } }) } func TestOptionsTimestampFormat(t *testing.T) { capt := &capture{} // This timestamp format contains none of the characters that are // considered placeholders, so will produce a constant result. sink := newSink(capt.Func, NewFormatter(Options{LogTimestamp: true, TimestampFormat: "TIMESTAMP"})) dSink, _ := sink.(logr.CallDepthLogSink) sink = dSink.WithCallDepth(1) sink.Info(0, "msg") expect := `"ts"="TIMESTAMP" "level"=0 "msg"="msg"` if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } } func TestOptionsLogInfoLevel(t *testing.T) { testCases := []struct { name string level *string expect string }{ { name: "custom key", level: ptrstr("info_level"), expect: `"info_level"=0 "msg"="msg"`, }, { name: "no level", level: ptrstr(""), expect: `"msg"="msg"`, }, { name: "default", level: nil, expect: `"level"=0 "msg"="msg"`, }, } for _, tc := range testCases { t.Run("Run: "+tc.name, func(t *testing.T) { capt := &capture{} sink := newSink(capt.Func, NewFormatter(Options{LogInfoLevel: tc.level})) dSink, _ := sink.(logr.CallDepthLogSink) sink = dSink.WithCallDepth(1) sink.Info(0, "msg") if capt.log != tc.expect { t.Errorf("\nexpected %q\n got %q", tc.expect, capt.log) } }) } } golang-github-go-logr-logr-1.4.2/funcr/slogsink.go000066400000000000000000000056241470525074300220650ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package funcr import ( "context" "log/slog" "github.com/go-logr/logr" ) var _ logr.SlogSink = &fnlogger{} const extraSlogSinkDepth = 3 // 2 for slog, 1 for SlogSink func (l fnlogger) Handle(_ context.Context, record slog.Record) error { kvList := make([]any, 0, 2*record.NumAttrs()) record.Attrs(func(attr slog.Attr) bool { kvList = attrToKVs(attr, kvList) return true }) if record.Level >= slog.LevelError { l.WithCallDepth(extraSlogSinkDepth).Error(nil, record.Message, kvList...) } else { level := l.levelFromSlog(record.Level) l.WithCallDepth(extraSlogSinkDepth).Info(level, record.Message, kvList...) } return nil } func (l fnlogger) WithAttrs(attrs []slog.Attr) logr.SlogSink { kvList := make([]any, 0, 2*len(attrs)) for _, attr := range attrs { kvList = attrToKVs(attr, kvList) } l.AddValues(kvList) return &l } func (l fnlogger) WithGroup(name string) logr.SlogSink { l.startGroup(name) return &l } // attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups // and other details of slog. func attrToKVs(attr slog.Attr, kvList []any) []any { attrVal := attr.Value.Resolve() if attrVal.Kind() == slog.KindGroup { groupVal := attrVal.Group() grpKVs := make([]any, 0, 2*len(groupVal)) for _, attr := range groupVal { grpKVs = attrToKVs(attr, grpKVs) } if attr.Key == "" { // slog says we have to inline these kvList = append(kvList, grpKVs...) } else { kvList = append(kvList, attr.Key, PseudoStruct(grpKVs)) } } else if attr.Key != "" { kvList = append(kvList, attr.Key, attrVal.Any()) } return kvList } // levelFromSlog adjusts the level by the logger's verbosity and negates it. // It ensures that the result is >= 0. This is necessary because the result is // passed to a LogSink and that API did not historically document whether // levels could be negative or what that meant. // // Some example usage: // // logrV0 := getMyLogger() // logrV2 := logrV0.V(2) // slogV2 := slog.New(logr.ToSlogHandler(logrV2)) // slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6) // slogV2.Info("msg") // =~ logrV2.V(0) =~ logrV0.V(2) // slogv2.Warn("msg") // =~ logrV2.V(-4) =~ logrV0.V(0) func (l fnlogger) levelFromSlog(level slog.Level) int { result := -level if result < 0 { result = 0 // because LogSink doesn't expect negative V levels } return int(result) } golang-github-go-logr-logr-1.4.2/funcr/slogsink_test.go000066400000000000000000000145501470525074300231220ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package funcr import ( "bytes" "fmt" "log/slog" "path/filepath" "runtime" "testing" "github.com/go-logr/logr" "github.com/go-logr/logr/internal/testhelp" ) func TestSlogSink(t *testing.T) { testCases := []struct { name string withAttrs []any withGroup string args []any expect string }{{ name: "just msg", args: makeKV(), expect: `{"logger":"","level":0,"msg":"msg"}`, }, { name: "primitives", args: makeKV("int", 1, "str", "ABC", "bool", true), expect: `{"logger":"","level":0,"msg":"msg","int":1,"str":"ABC","bool":true}`, }, { name: "with attrs", withAttrs: makeKV("attrInt", 1, "attrStr", "ABC", "attrBool", true), args: makeKV("int", 2), expect: `{"logger":"","level":0,"msg":"msg","attrInt":1,"attrStr":"ABC","attrBool":true,"int":2}`, }, { name: "with group", withGroup: "groupname", args: makeKV("int", 1, "str", "ABC", "bool", true), expect: `{"logger":"","level":0,"msg":"msg","groupname":{"int":1,"str":"ABC","bool":true}}`, }, { name: "with attrs and group", withAttrs: makeKV("attrInt", 1, "attrStr", "ABC"), withGroup: "groupname", args: makeKV("int", 3, "bool", true), expect: `{"logger":"","level":0,"msg":"msg","attrInt":1,"attrStr":"ABC","groupname":{"int":3,"bool":true}}`, }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { capt := &capture{} logger := logr.New(newSink(capt.Func, NewFormatterJSON(Options{}))) slogger := slog.New(logr.ToSlogHandler(logger)) if len(tc.withAttrs) > 0 { slogger = slogger.With(tc.withAttrs...) } if tc.withGroup != "" { slogger = slogger.WithGroup(tc.withGroup) } slogger.Info("msg", tc.args...) if capt.log != tc.expect { t.Errorf("\nexpected %q\n got %q", tc.expect, capt.log) } }) } } func TestSlogSinkGroups(t *testing.T) { testCases := []struct { name string fn func(slogger *slog.Logger) expect string }{{ name: "no group", fn: func(slogger *slog.Logger) { slogger. Info("msg", "k", "v") }, expect: `{"logger":"","level":0,"msg":"msg","k":"v"}`, }, { name: "1 group with leaf args", fn: func(slogger *slog.Logger) { slogger. WithGroup("g1"). Info("msg", "k", "v") }, expect: `{"logger":"","level":0,"msg":"msg","g1":{"k":"v"}}`, }, { name: "1 group without leaf args", fn: func(slogger *slog.Logger) { slogger. WithGroup("g1"). Info("msg") }, expect: `{"logger":"","level":0,"msg":"msg"}`, }, { name: "1 group with value without leaf args", fn: func(slogger *slog.Logger) { slogger. WithGroup("g1").With("k1", 1). Info("msg") }, expect: `{"logger":"","level":0,"msg":"msg","g1":{"k1":1}}`, }, { name: "2 groups with values no leaf args", fn: func(slogger *slog.Logger) { slogger. WithGroup("g1").With("k1", 1). WithGroup("g2").With("k2", 2). Info("msg") }, expect: `{"logger":"","level":0,"msg":"msg","g1":{"k1":1,"g2":{"k2":2}}}`, }, { name: "3 empty groups with no values or leaf args", fn: func(slogger *slog.Logger) { slogger. WithGroup("g1"). WithGroup("g2"). WithGroup("g3"). Info("msg") }, expect: `{"logger":"","level":0,"msg":"msg"}`, }, { name: "3 empty groups with no values but with leaf args", fn: func(slogger *slog.Logger) { slogger. WithGroup("g1"). WithGroup("g2"). WithGroup("g3"). Info("msg", "k", "v") }, expect: `{"logger":"","level":0,"msg":"msg","g1":{"g2":{"g3":{"k":"v"}}}}`, }, { name: "multiple groups with and without values", fn: func(slogger *slog.Logger) { slogger. With("k0", 0). WithGroup("g1"). WithGroup("g2"). WithGroup("g3").With("k3", 3). WithGroup("g4"). WithGroup("g5"). WithGroup("g6").With("k6", 6). WithGroup("g7"). WithGroup("g8"). WithGroup("g9"). Info("msg") }, expect: `{"logger":"","level":0,"msg":"msg","k0":0,"g1":{"g2":{"g3":{"k3":3,"g4":{"g5":{"g6":{"k6":6}}}}}}}`, }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { capt := &capture{} logger := logr.New(newSink(capt.Func, NewFormatterJSON(Options{}))) slogger := slog.New(logr.ToSlogHandler(logger)) tc.fn(slogger) if capt.log != tc.expect { t.Errorf("\nexpected: `%s`\n got: `%s`", tc.expect, capt.log) } }) } } func TestSlogSinkWithCaller(t *testing.T) { capt := &capture{} logger := logr.New(newSink(capt.Func, NewFormatterJSON(Options{LogCaller: All}))) slogger := slog.New(logr.ToSlogHandler(logger)) slogger.Error("msg", "int", 1) _, file, line, _ := runtime.Caller(0) expect := fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d},"msg":"msg","error":null,"int":1}`, filepath.Base(file), line-1) if capt.log != expect { t.Errorf("\nexpected %q\n got %q", expect, capt.log) } } func TestRunSlogTests(t *testing.T) { fn := func(buffer *bytes.Buffer) slog.Handler { printfn := func(obj string) { fmt.Fprintln(buffer, obj) } opts := Options{ LogTimestamp: true, Verbosity: 10, RenderBuiltinsHook: func(kvList []any) []any { mappedKVList := make([]any, len(kvList)) for i := 0; i < len(kvList); i += 2 { key := kvList[i] switch key { case "ts": mappedKVList[i] = "time" default: mappedKVList[i] = key } mappedKVList[i+1] = kvList[i+1] } return mappedKVList }, } logger := NewJSON(printfn, opts) return logr.ToSlogHandler(logger) } exceptions := []string{ "a Handler should ignore a zero Record.Time", // Time is generated by sink. } testhelp.RunSlogTests(t, fn, exceptions...) } func TestLogrSlogConversion(t *testing.T) { f := New(func(_, _ string) {}, Options{}) f2 := logr.FromSlogHandler(logr.ToSlogHandler(f)) if want, got := f, f2; got != want { t.Helper() t.Errorf("Expected %T %+v, got instead: %T %+v", want, want, got, got) } } golang-github-go-logr-logr-1.4.2/go.mod000066400000000000000000000000501470525074300176620ustar00rootroot00000000000000module github.com/go-logr/logr go 1.18 golang-github-go-logr-logr-1.4.2/internal/000077500000000000000000000000001470525074300203755ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/internal/testhelp/000077500000000000000000000000001470525074300222255ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/internal/testhelp/slog.go000066400000000000000000000042051470525074300235210ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package testhelp holds helper functions for the testing of logr and built-in // implementations. package testhelp import ( "bytes" "encoding/json" "log/slog" "strings" "testing" "testing/slogtest" ) // RunSlogTests runs slogtest.TestHandler on a given slog.Handler, which is // expected to emit JSON into the provided buffer. func RunSlogTests(t *testing.T, createHandler func(buffer *bytes.Buffer) slog.Handler, exceptions ...string) { var buffer bytes.Buffer handler := createHandler(&buffer) err := slogtest.TestHandler(handler, func() []map[string]any { var ms []map[string]any for _, line := range bytes.Split(buffer.Bytes(), []byte{'\n'}) { if len(line) == 0 { continue } var m map[string]any if err := json.Unmarshal(line, &m); err != nil { t.Errorf("%v: %q", err, string(line)) } ms = append(ms, m) } return ms }) // Correlating failures with individual test cases is hard with the current API. // See https://github.com/golang/go/issues/61758 t.Logf("Output:\n%s", buffer.String()) if err != nil { if unwrappable, ok := err.(interface { Unwrap() []error }); ok { for _, err := range unwrappable.Unwrap() { if !containsOne(err.Error(), exceptions...) { t.Errorf("Unexpected error: %v", err) } } } else { // Shouldn't be reached, errors from errors.Join can be split up. t.Errorf("Unexpected errors:\n%v", err) } } } func containsOne(hay string, needles ...string) bool { for _, needle := range needles { if strings.Contains(hay, needle) { return true } } return false } golang-github-go-logr-logr-1.4.2/internal/testhelp/slog_test.go000066400000000000000000000015331470525074300245610ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package testhelp import ( "bytes" "log/slog" "testing" ) func TestRunSlogTestsOnSlogSink(t *testing.T) { // This proves that RunSlogTests works. RunSlogTests(t, func(buffer *bytes.Buffer) slog.Handler { return slog.NewJSONHandler(buffer, nil) }) } golang-github-go-logr-logr-1.4.2/logr.go000066400000000000000000000503431470525074300200600ustar00rootroot00000000000000/* Copyright 2019 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // This design derives from Dave Cheney's blog: // http://dave.cheney.net/2015/11/05/lets-talk-about-logging // Package logr defines a general-purpose logging API and abstract interfaces // to back that API. Packages in the Go ecosystem can depend on this package, // while callers can implement logging with whatever backend is appropriate. // // # Usage // // Logging is done using a Logger instance. Logger is a concrete type with // methods, which defers the actual logging to a LogSink interface. The main // methods of Logger are Info() and Error(). Arguments to Info() and Error() // are key/value pairs rather than printf-style formatted strings, emphasizing // "structured logging". // // With Go's standard log package, we might write: // // log.Printf("setting target value %s", targetValue) // // With logr's structured logging, we'd write: // // logger.Info("setting target", "value", targetValue) // // Errors are much the same. Instead of: // // log.Printf("failed to open the pod bay door for user %s: %v", user, err) // // We'd write: // // logger.Error(err, "failed to open the pod bay door", "user", user) // // Info() and Error() are very similar, but they are separate methods so that // LogSink implementations can choose to do things like attach additional // information (such as stack traces) on calls to Error(). Error() messages are // always logged, regardless of the current verbosity. If there is no error // instance available, passing nil is valid. // // # Verbosity // // Often we want to log information only when the application in "verbose // mode". To write log lines that are more verbose, Logger has a V() method. // The higher the V-level of a log line, the less critical it is considered. // Log-lines with V-levels that are not enabled (as per the LogSink) will not // be written. Level V(0) is the default, and logger.V(0).Info() has the same // meaning as logger.Info(). Negative V-levels have the same meaning as V(0). // Error messages do not have a verbosity level and are always logged. // // Where we might have written: // // if flVerbose >= 2 { // log.Printf("an unusual thing happened") // } // // We can write: // // logger.V(2).Info("an unusual thing happened") // // # Logger Names // // Logger instances can have name strings so that all messages logged through // that instance have additional context. For example, you might want to add // a subsystem name: // // logger.WithName("compactor").Info("started", "time", time.Now()) // // The WithName() method returns a new Logger, which can be passed to // constructors or other functions for further use. Repeated use of WithName() // will accumulate name "segments". These name segments will be joined in some // way by the LogSink implementation. It is strongly recommended that name // segments contain simple identifiers (letters, digits, and hyphen), and do // not contain characters that could muddle the log output or confuse the // joining operation (e.g. whitespace, commas, periods, slashes, brackets, // quotes, etc). // // # Saved Values // // Logger instances can store any number of key/value pairs, which will be // logged alongside all messages logged through that instance. For example, // you might want to create a Logger instance per managed object: // // With the standard log package, we might write: // // log.Printf("decided to set field foo to value %q for object %s/%s", // targetValue, object.Namespace, object.Name) // // With logr we'd write: // // // Elsewhere: set up the logger to log the object name. // obj.logger = mainLogger.WithValues( // "name", obj.name, "namespace", obj.namespace) // // // later on... // obj.logger.Info("setting foo", "value", targetValue) // // # Best Practices // // Logger has very few hard rules, with the goal that LogSink implementations // might have a lot of freedom to differentiate. There are, however, some // things to consider. // // The log message consists of a constant message attached to the log line. // This should generally be a simple description of what's occurring, and should // never be a format string. Variable information can then be attached using // named values. // // Keys are arbitrary strings, but should generally be constant values. Values // may be any Go value, but how the value is formatted is determined by the // LogSink implementation. // // Logger instances are meant to be passed around by value. Code that receives // such a value can call its methods without having to check whether the // instance is ready for use. // // The zero logger (= Logger{}) is identical to Discard() and discards all log // entries. Code that receives a Logger by value can simply call it, the methods // will never crash. For cases where passing a logger is optional, a pointer to Logger // should be used. // // # Key Naming Conventions // // Keys are not strictly required to conform to any specification or regex, but // it is recommended that they: // - be human-readable and meaningful (not auto-generated or simple ordinals) // - be constant (not dependent on input data) // - contain only printable characters // - not contain whitespace or punctuation // - use lower case for simple keys and lowerCamelCase for more complex ones // // These guidelines help ensure that log data is processed properly regardless // of the log implementation. For example, log implementations will try to // output JSON data or will store data for later database (e.g. SQL) queries. // // While users are generally free to use key names of their choice, it's // generally best to avoid using the following keys, as they're frequently used // by implementations: // - "caller": the calling information (file/line) of a particular log line // - "error": the underlying error value in the `Error` method // - "level": the log level // - "logger": the name of the associated logger // - "msg": the log message // - "stacktrace": the stack trace associated with a particular log line or // error (often from the `Error` message) // - "ts": the timestamp for a log line // // Implementations are encouraged to make use of these keys to represent the // above concepts, when necessary (for example, in a pure-JSON output form, it // would be necessary to represent at least message and timestamp as ordinary // named values). // // # Break Glass // // Implementations may choose to give callers access to the underlying // logging implementation. The recommended pattern for this is: // // // Underlier exposes access to the underlying logging implementation. // // Since callers only have a logr.Logger, they have to know which // // implementation is in use, so this interface is less of an abstraction // // and more of way to test type conversion. // type Underlier interface { // GetUnderlying() // } // // Logger grants access to the sink to enable type assertions like this: // // func DoSomethingWithImpl(log logr.Logger) { // if underlier, ok := log.GetSink().(impl.Underlier); ok { // implLogger := underlier.GetUnderlying() // ... // } // } // // Custom `With*` functions can be implemented by copying the complete // Logger struct and replacing the sink in the copy: // // // WithFooBar changes the foobar parameter in the log sink and returns a // // new logger with that modified sink. It does nothing for loggers where // // the sink doesn't support that parameter. // func WithFoobar(log logr.Logger, foobar int) logr.Logger { // if foobarLogSink, ok := log.GetSink().(FoobarSink); ok { // log = log.WithSink(foobarLogSink.WithFooBar(foobar)) // } // return log // } // // Don't use New to construct a new Logger with a LogSink retrieved from an // existing Logger. Source code attribution might not work correctly and // unexported fields in Logger get lost. // // Beware that the same LogSink instance may be shared by different logger // instances. Calling functions that modify the LogSink will affect all of // those. package logr // New returns a new Logger instance. This is primarily used by libraries // implementing LogSink, rather than end users. Passing a nil sink will create // a Logger which discards all log lines. func New(sink LogSink) Logger { logger := Logger{} logger.setSink(sink) if sink != nil { sink.Init(runtimeInfo) } return logger } // setSink stores the sink and updates any related fields. It mutates the // logger and thus is only safe to use for loggers that are not currently being // used concurrently. func (l *Logger) setSink(sink LogSink) { l.sink = sink } // GetSink returns the stored sink. func (l Logger) GetSink() LogSink { return l.sink } // WithSink returns a copy of the logger with the new sink. func (l Logger) WithSink(sink LogSink) Logger { l.setSink(sink) return l } // Logger is an interface to an abstract logging implementation. This is a // concrete type for performance reasons, but all the real work is passed on to // a LogSink. Implementations of LogSink should provide their own constructors // that return Logger, not LogSink. // // The underlying sink can be accessed through GetSink and be modified through // WithSink. This enables the implementation of custom extensions (see "Break // Glass" in the package documentation). Normally the sink should be used only // indirectly. type Logger struct { sink LogSink level int } // Enabled tests whether this Logger is enabled. For example, commandline // flags might be used to set the logging verbosity and disable some info logs. func (l Logger) Enabled() bool { // Some implementations of LogSink look at the caller in Enabled (e.g. // different verbosity levels per package or file), but we only pass one // CallDepth in (via Init). This means that all calls from Logger to the // LogSink's Enabled, Info, and Error methods must have the same number of // frames. In other words, Logger methods can't call other Logger methods // which call these LogSink methods unless we do it the same in all paths. return l.sink != nil && l.sink.Enabled(l.level) } // Info logs a non-error message with the given key/value pairs as context. // // The msg argument should be used to add some constant description to the log // line. The key/value pairs can then be used to add additional variable // information. The key/value pairs must alternate string keys and arbitrary // values. func (l Logger) Info(msg string, keysAndValues ...any) { if l.sink == nil { return } if l.sink.Enabled(l.level) { // see comment in Enabled if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { withHelper.GetCallStackHelper()() } l.sink.Info(l.level, msg, keysAndValues...) } } // Error logs an error, with the given message and key/value pairs as context. // It functions similarly to Info, but may have unique behavior, and should be // preferred for logging errors (see the package documentations for more // information). The log message will always be emitted, regardless of // verbosity level. // // The msg argument should be used to add context to any underlying error, // while the err argument should be used to attach the actual error that // triggered this log line, if present. The err parameter is optional // and nil may be passed instead of an error instance. func (l Logger) Error(err error, msg string, keysAndValues ...any) { if l.sink == nil { return } if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { withHelper.GetCallStackHelper()() } l.sink.Error(err, msg, keysAndValues...) } // V returns a new Logger instance for a specific verbosity level, relative to // this Logger. In other words, V-levels are additive. A higher verbosity // level means a log message is less important. Negative V-levels are treated // as 0. func (l Logger) V(level int) Logger { if l.sink == nil { return l } if level < 0 { level = 0 } l.level += level return l } // GetV returns the verbosity level of the logger. If the logger's LogSink is // nil as in the Discard logger, this will always return 0. func (l Logger) GetV() int { // 0 if l.sink nil because of the if check in V above. return l.level } // WithValues returns a new Logger instance with additional key/value pairs. // See Info for documentation on how key/value pairs work. func (l Logger) WithValues(keysAndValues ...any) Logger { if l.sink == nil { return l } l.setSink(l.sink.WithValues(keysAndValues...)) return l } // WithName returns a new Logger instance with the specified name element added // to the Logger's name. Successive calls with WithName append additional // suffixes to the Logger's name. It's strongly recommended that name segments // contain only letters, digits, and hyphens (see the package documentation for // more information). func (l Logger) WithName(name string) Logger { if l.sink == nil { return l } l.setSink(l.sink.WithName(name)) return l } // WithCallDepth returns a Logger instance that offsets the call stack by the // specified number of frames when logging call site information, if possible. // This is useful for users who have helper functions between the "real" call // site and the actual calls to Logger methods. If depth is 0 the attribution // should be to the direct caller of this function. If depth is 1 the // attribution should skip 1 call frame, and so on. Successive calls to this // are additive. // // If the underlying log implementation supports a WithCallDepth(int) method, // it will be called and the result returned. If the implementation does not // support CallDepthLogSink, the original Logger will be returned. // // To skip one level, WithCallStackHelper() should be used instead of // WithCallDepth(1) because it works with implementions that support the // CallDepthLogSink and/or CallStackHelperLogSink interfaces. func (l Logger) WithCallDepth(depth int) Logger { if l.sink == nil { return l } if withCallDepth, ok := l.sink.(CallDepthLogSink); ok { l.setSink(withCallDepth.WithCallDepth(depth)) } return l } // WithCallStackHelper returns a new Logger instance that skips the direct // caller when logging call site information, if possible. This is useful for // users who have helper functions between the "real" call site and the actual // calls to Logger methods and want to support loggers which depend on marking // each individual helper function, like loggers based on testing.T. // // In addition to using that new logger instance, callers also must call the // returned function. // // If the underlying log implementation supports a WithCallDepth(int) method, // WithCallDepth(1) will be called to produce a new logger. If it supports a // WithCallStackHelper() method, that will be also called. If the // implementation does not support either of these, the original Logger will be // returned. func (l Logger) WithCallStackHelper() (func(), Logger) { if l.sink == nil { return func() {}, l } var helper func() if withCallDepth, ok := l.sink.(CallDepthLogSink); ok { l.setSink(withCallDepth.WithCallDepth(1)) } if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { helper = withHelper.GetCallStackHelper() } else { helper = func() {} } return helper, l } // IsZero returns true if this logger is an uninitialized zero value func (l Logger) IsZero() bool { return l.sink == nil } // RuntimeInfo holds information that the logr "core" library knows which // LogSinks might want to know. type RuntimeInfo struct { // CallDepth is the number of call frames the logr library adds between the // end-user and the LogSink. LogSink implementations which choose to print // the original logging site (e.g. file & line) should climb this many // additional frames to find it. CallDepth int } // runtimeInfo is a static global. It must not be changed at run time. var runtimeInfo = RuntimeInfo{ CallDepth: 1, } // LogSink represents a logging implementation. End-users will generally not // interact with this type. type LogSink interface { // Init receives optional information about the logr library for LogSink // implementations that need it. Init(info RuntimeInfo) // Enabled tests whether this LogSink is enabled at the specified V-level. // For example, commandline flags might be used to set the logging // verbosity and disable some info logs. Enabled(level int) bool // Info logs a non-error message with the given key/value pairs as context. // The level argument is provided for optional logging. This method will // only be called when Enabled(level) is true. See Logger.Info for more // details. Info(level int, msg string, keysAndValues ...any) // Error logs an error, with the given message and key/value pairs as // context. See Logger.Error for more details. Error(err error, msg string, keysAndValues ...any) // WithValues returns a new LogSink with additional key/value pairs. See // Logger.WithValues for more details. WithValues(keysAndValues ...any) LogSink // WithName returns a new LogSink with the specified name appended. See // Logger.WithName for more details. WithName(name string) LogSink } // CallDepthLogSink represents a LogSink that knows how to climb the call stack // to identify the original call site and can offset the depth by a specified // number of frames. This is useful for users who have helper functions // between the "real" call site and the actual calls to Logger methods. // Implementations that log information about the call site (such as file, // function, or line) would otherwise log information about the intermediate // helper functions. // // This is an optional interface and implementations are not required to // support it. type CallDepthLogSink interface { // WithCallDepth returns a LogSink that will offset the call // stack by the specified number of frames when logging call // site information. // // If depth is 0, the LogSink should skip exactly the number // of call frames defined in RuntimeInfo.CallDepth when Info // or Error are called, i.e. the attribution should be to the // direct caller of Logger.Info or Logger.Error. // // If depth is 1 the attribution should skip 1 call frame, and so on. // Successive calls to this are additive. WithCallDepth(depth int) LogSink } // CallStackHelperLogSink represents a LogSink that knows how to climb // the call stack to identify the original call site and can skip // intermediate helper functions if they mark themselves as // helper. Go's testing package uses that approach. // // This is useful for users who have helper functions between the // "real" call site and the actual calls to Logger methods. // Implementations that log information about the call site (such as // file, function, or line) would otherwise log information about the // intermediate helper functions. // // This is an optional interface and implementations are not required // to support it. Implementations that choose to support this must not // simply implement it as WithCallDepth(1), because // Logger.WithCallStackHelper will call both methods if they are // present. This should only be implemented for LogSinks that actually // need it, as with testing.T. type CallStackHelperLogSink interface { // GetCallStackHelper returns a function that must be called // to mark the direct caller as helper function when logging // call site information. GetCallStackHelper() func() } // Marshaler is an optional interface that logged values may choose to // implement. Loggers with structured output, such as JSON, should // log the object return by the MarshalLog method instead of the // original value. type Marshaler interface { // MarshalLog can be used to: // - ensure that structs are not logged as strings when the original // value has a String method: return a different type without a // String method // - select which fields of a complex type should get logged: // return a simpler struct with fewer fields // - log unexported fields: return a different struct // with exported fields // // It may return any value of any type. MarshalLog() any } golang-github-go-logr-logr-1.4.2/logr_test.go000066400000000000000000000240051470525074300211130ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "errors" "fmt" "reflect" "runtime" "testing" ) func TestNew(t *testing.T) { calledInit := 0 sink := &testLogSink{} sink.fnInit = func(ri RuntimeInfo) { if ri.CallDepth != 1 { t.Errorf("expected runtimeInfo.CallDepth = 1, got %d", ri.CallDepth) } calledInit++ } logger := New(sink) if logger.sink == nil { t.Errorf("expected sink to be set, got %v", logger.sink) } if calledInit != 1 { t.Errorf("expected sink.Init() to be called once, got %d", calledInit) } if _, ok := logger.sink.(CallDepthLogSink); ok { t.Errorf("expected conversion to CallDepthLogSink to fail") } } func TestNewCachesCallDepthInterface(t *testing.T) { sink := &testCallDepthLogSink{} logger := New(sink) if _, ok := logger.sink.(CallDepthLogSink); !ok { t.Errorf("expected conversion to CallDepthLogSink to succeed") } } func TestEnabled(t *testing.T) { calledEnabled := 0 sink := &testLogSink{} sink.fnEnabled = func(_ int) bool { calledEnabled++ return true } logger := New(sink) if en := logger.Enabled(); en != true { t.Errorf("expected true") } if calledEnabled != 1 { t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) } } func TestError(t *testing.T) { calledError := 0 errInput := fmt.Errorf("error") msgInput := "msg" kvInput := []any{0, 1, 2} sink := &testLogSink{} sink.fnError = func(err error, msg string, kv ...any) { calledError++ if err != errInput { t.Errorf("unexpected err input, got %v", err) } if msg != msgInput { t.Errorf("unexpected msg input, got %q", msg) } if !reflect.DeepEqual(kv, kvInput) { t.Errorf("unexpected kv input, got %v", kv) } } logger := New(sink) logger.Error(errInput, msgInput, kvInput...) if calledError != 1 { t.Errorf("expected sink.Error() to be called once, got %d", calledError) } } func TestV(t *testing.T) { for name, logger := range map[string]Logger{ "testLogSink": New(&testLogSink{}), "Discard": Discard(), "Zero": {}, } { t.Run(name, func(t *testing.T) { adjust := func(level int) int { if logger.GetSink() == nil { // The Discard and the zero Logger short-cut the V call and don't // change the verbosity level. return 0 } return level } inputs := []struct { name string fn func() Logger exp int }{{ name: "V(0)", fn: func() Logger { return logger.V(0) }, exp: 0, }, { name: "V(93)", fn: func() Logger { return logger.V(93) }, exp: adjust(93), }, { name: "V(70).V(6)", fn: func() Logger { return logger.V(70).V(6) }, exp: adjust(76), }, { name: "V(-1)", fn: func() Logger { return logger.V(-1) }, exp: 0, }, { name: "V(1).V(-1)", fn: func() Logger { return logger.V(1).V(-1) }, exp: adjust(1), }} for _, in := range inputs { t.Run(in.name, func(t *testing.T) { if want, got := in.exp, in.fn().GetV(); got != want { t.Errorf("expected %d, got %d", want, got) } }) } }) } } func TestInfo(t *testing.T) { calledEnabled := 0 calledInfo := 0 lvlInput := 0 msgInput := "msg" kvInput := []any{0, 1, 2} sink := &testLogSink{} sink.fnEnabled = func(lvl int) bool { calledEnabled++ return lvl < 100 } sink.fnInfo = func(lvl int, msg string, kv ...any) { calledInfo++ if lvl != lvlInput { t.Errorf("unexpected lvl input, got %v", lvl) } if msg != msgInput { t.Errorf("unexpected msg input, got %q", msg) } if !reflect.DeepEqual(kv, kvInput) { t.Errorf("unexpected kv input, got %v", kv) } } logger := New(sink) calledEnabled = 0 calledInfo = 0 lvlInput = 0 logger.Info(msgInput, kvInput...) if calledEnabled != 1 { t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) } if calledInfo != 1 { t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) } calledEnabled = 0 calledInfo = 0 lvlInput = 0 logger.V(0).Info(msgInput, kvInput...) if calledEnabled != 1 { t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) } if calledInfo != 1 { t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) } calledEnabled = 0 calledInfo = 0 lvlInput = 93 logger.V(93).Info(msgInput, kvInput...) if calledEnabled != 1 { t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) } if calledInfo != 1 { t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) } calledEnabled = 0 calledInfo = 0 lvlInput = 100 logger.V(100).Info(msgInput, kvInput...) if calledEnabled != 1 { t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) } if calledInfo != 0 { t.Errorf("expected sink.Info() to not be called, got %d", calledInfo) } } func TestWithValues(t *testing.T) { calledWithValues := 0 kvInput := []any{"zero", 0, "one", 1, "two", 2} sink := &testLogSink{} sink.fnWithValues = func(kv ...any) { calledWithValues++ if !reflect.DeepEqual(kv, kvInput) { t.Errorf("unexpected kv input, got %v", kv) } } logger := New(sink) out := logger.WithValues(kvInput...) if calledWithValues != 1 { t.Errorf("expected sink.WithValues() to be called once, got %d", calledWithValues) } if p, _ := out.sink.(*testLogSink); p == sink { t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) } } func TestWithName(t *testing.T) { calledWithName := 0 nameInput := "name" sink := &testLogSink{} sink.fnWithName = func(name string) { calledWithName++ if name != nameInput { t.Errorf("unexpected name input, got %q", name) } } logger := New(sink) out := logger.WithName(nameInput) if calledWithName != 1 { t.Errorf("expected sink.WithName() to be called once, got %d", calledWithName) } if p, _ := out.sink.(*testLogSink); p == sink { t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) } } func TestWithCallDepthNotImplemented(t *testing.T) { depthInput := 7 sink := &testLogSink{} logger := New(sink) out := logger.WithCallDepth(depthInput) if p, _ := out.sink.(*testLogSink); p != sink { t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) } } func TestWithCallDepthImplemented(t *testing.T) { calledWithCallDepth := 0 depthInput := 7 sink := &testCallDepthLogSink{} sink.fnWithCallDepth = func(depth int) { calledWithCallDepth++ if depth != depthInput { t.Errorf("unexpected depth input, got %d", depth) } } logger := New(sink) out := logger.WithCallDepth(depthInput) if calledWithCallDepth != 1 { t.Errorf("expected sink.WithCallDepth() to be called once, got %d", calledWithCallDepth) } p, _ := out.sink.(*testCallDepthLogSink) if p == sink { t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) } if p.callDepth != depthInput { t.Errorf("expected sink to have call depth %d, got %d", depthInput, p.callDepth) } } func TestWithCallDepthIncremental(t *testing.T) { calledWithCallDepth := 0 depthInput := 7 sink := &testCallDepthLogSink{} sink.fnWithCallDepth = func(depth int) { calledWithCallDepth++ if depth != 1 { t.Errorf("unexpected depth input, got %d", depth) } } logger := New(sink) out := logger for i := 0; i < depthInput; i++ { out = out.WithCallDepth(1) } if calledWithCallDepth != depthInput { t.Errorf("expected sink.WithCallDepth() to be called %d times, got %d", depthInput, calledWithCallDepth) } p, _ := out.sink.(*testCallDepthLogSink) if p == sink { t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) } if p.callDepth != depthInput { t.Errorf("expected sink to have call depth %d, got %d", depthInput, p.callDepth) } } func TestIsZero(t *testing.T) { var l Logger if !l.IsZero() { t.Errorf("expected IsZero") } sink := &testLogSink{} l = New(sink) if l.IsZero() { t.Errorf("expected not IsZero") } // Discard is the same as a nil sink. l = Discard() if !l.IsZero() { t.Errorf("expected IsZero") } } func TestZeroValue(t *testing.T) { // Make sure that the zero value is useful and equivalent to a Discard logger. var l Logger if l.Enabled() { t.Errorf("expected not Enabled") } if !l.IsZero() { t.Errorf("expected IsZero") } // Make sure that none of these methods cause a crash l.Info("foo") l.Error(errors.New("bar"), "some error") if l.GetSink() != nil { t.Errorf("expected nil from GetSink") } l2 := l.WithName("some-name").V(2).WithValues("foo", 1).WithCallDepth(1) l2.Info("foo") l2.Error(errors.New("bar"), "some error") _, _ = l.WithCallStackHelper() } func TestCallDepthConsistent(t *testing.T) { sink := &testLogSink{} depth := 0 expect := "github.com/go-logr/logr.TestCallDepthConsistent" sink.fnInit = func(ri RuntimeInfo) { depth = ri.CallDepth + 1 // 1 for these function pointers if caller := getCaller(depth); caller != expect { t.Errorf("identified wrong caller %q", caller) } } sink.fnEnabled = func(_ int) bool { if caller := getCaller(depth); caller != expect { t.Errorf("identified wrong caller %q", caller) } return true } sink.fnError = func(_ error, _ string, _ ...any) { if caller := getCaller(depth); caller != expect { t.Errorf("identified wrong caller %q", caller) } } l := New(sink) l.Enabled() l.Info("msg") l.Error(nil, "msg") } func getCaller(depth int) string { // +1 for this frame, +1 for Info/Error/Enabled. pc, _, _, ok := runtime.Caller(depth + 2) if !ok { return "" } fp := runtime.FuncForPC(pc) if fp == nil { return "" } return fp.Name() } golang-github-go-logr-logr-1.4.2/sloghandler.go000066400000000000000000000131241470525074300214130ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "context" "log/slog" ) type slogHandler struct { // May be nil, in which case all logs get discarded. sink LogSink // Non-nil if sink is non-nil and implements SlogSink. slogSink SlogSink // groupPrefix collects values from WithGroup calls. It gets added as // prefix to value keys when handling a log record. groupPrefix string // levelBias can be set when constructing the handler to influence the // slog.Level of log records. A positive levelBias reduces the // slog.Level value. slog has no API to influence this value after the // handler got created, so it can only be set indirectly through // Logger.V. levelBias slog.Level } var _ slog.Handler = &slogHandler{} // groupSeparator is used to concatenate WithGroup names and attribute keys. const groupSeparator = "." // GetLevel is used for black box unit testing. func (l *slogHandler) GetLevel() slog.Level { return l.levelBias } func (l *slogHandler) Enabled(_ context.Context, level slog.Level) bool { return l.sink != nil && (level >= slog.LevelError || l.sink.Enabled(l.levelFromSlog(level))) } func (l *slogHandler) Handle(ctx context.Context, record slog.Record) error { if l.slogSink != nil { // Only adjust verbosity level of log entries < slog.LevelError. if record.Level < slog.LevelError { record.Level -= l.levelBias } return l.slogSink.Handle(ctx, record) } // No need to check for nil sink here because Handle will only be called // when Enabled returned true. kvList := make([]any, 0, 2*record.NumAttrs()) record.Attrs(func(attr slog.Attr) bool { kvList = attrToKVs(attr, l.groupPrefix, kvList) return true }) if record.Level >= slog.LevelError { l.sinkWithCallDepth().Error(nil, record.Message, kvList...) } else { level := l.levelFromSlog(record.Level) l.sinkWithCallDepth().Info(level, record.Message, kvList...) } return nil } // sinkWithCallDepth adjusts the stack unwinding so that when Error or Info // are called by Handle, code in slog gets skipped. // // This offset currently (Go 1.21.0) works for calls through // slog.New(ToSlogHandler(...)). There's no guarantee that the call // chain won't change. Wrapping the handler will also break unwinding. It's // still better than not adjusting at all.... // // This cannot be done when constructing the handler because FromSlogHandler needs // access to the original sink without this adjustment. A second copy would // work, but then WithAttrs would have to be called for both of them. func (l *slogHandler) sinkWithCallDepth() LogSink { if sink, ok := l.sink.(CallDepthLogSink); ok { return sink.WithCallDepth(2) } return l.sink } func (l *slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if l.sink == nil || len(attrs) == 0 { return l } clone := *l if l.slogSink != nil { clone.slogSink = l.slogSink.WithAttrs(attrs) clone.sink = clone.slogSink } else { kvList := make([]any, 0, 2*len(attrs)) for _, attr := range attrs { kvList = attrToKVs(attr, l.groupPrefix, kvList) } clone.sink = l.sink.WithValues(kvList...) } return &clone } func (l *slogHandler) WithGroup(name string) slog.Handler { if l.sink == nil { return l } if name == "" { // slog says to inline empty groups return l } clone := *l if l.slogSink != nil { clone.slogSink = l.slogSink.WithGroup(name) clone.sink = clone.slogSink } else { clone.groupPrefix = addPrefix(clone.groupPrefix, name) } return &clone } // attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups // and other details of slog. func attrToKVs(attr slog.Attr, groupPrefix string, kvList []any) []any { attrVal := attr.Value.Resolve() if attrVal.Kind() == slog.KindGroup { groupVal := attrVal.Group() grpKVs := make([]any, 0, 2*len(groupVal)) prefix := groupPrefix if attr.Key != "" { prefix = addPrefix(groupPrefix, attr.Key) } for _, attr := range groupVal { grpKVs = attrToKVs(attr, prefix, grpKVs) } kvList = append(kvList, grpKVs...) } else if attr.Key != "" { kvList = append(kvList, addPrefix(groupPrefix, attr.Key), attrVal.Any()) } return kvList } func addPrefix(prefix, name string) string { if prefix == "" { return name } if name == "" { return prefix } return prefix + groupSeparator + name } // levelFromSlog adjusts the level by the logger's verbosity and negates it. // It ensures that the result is >= 0. This is necessary because the result is // passed to a LogSink and that API did not historically document whether // levels could be negative or what that meant. // // Some example usage: // // logrV0 := getMyLogger() // logrV2 := logrV0.V(2) // slogV2 := slog.New(logr.ToSlogHandler(logrV2)) // slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6) // slogV2.Info("msg") // =~ logrV2.V(0) =~ logrV0.V(2) // slogv2.Warn("msg") // =~ logrV2.V(-4) =~ logrV0.V(0) func (l *slogHandler) levelFromSlog(level slog.Level) int { result := -level result += l.levelBias // in case the original Logger had a V level if result < 0 { result = 0 // because LogSink doesn't expect negative V levels } return int(result) } golang-github-go-logr-logr-1.4.2/slogr.go000066400000000000000000000072171470525074300202450ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "context" "log/slog" ) // FromSlogHandler returns a Logger which writes to the slog.Handler. // // The logr verbosity level is mapped to slog levels such that V(0) becomes // slog.LevelInfo and V(4) becomes slog.LevelDebug. func FromSlogHandler(handler slog.Handler) Logger { if handler, ok := handler.(*slogHandler); ok { if handler.sink == nil { return Discard() } return New(handler.sink).V(int(handler.levelBias)) } return New(&slogSink{handler: handler}) } // ToSlogHandler returns a slog.Handler which writes to the same sink as the Logger. // // The returned logger writes all records with level >= slog.LevelError as // error log entries with LogSink.Error, regardless of the verbosity level of // the Logger: // // logger := // slog.New(ToSlogHandler(logger.V(10))).Error(...) -> logSink.Error(...) // // The level of all other records gets reduced by the verbosity // level of the Logger and the result is negated. If it happens // to be negative, then it gets replaced by zero because a LogSink // is not expected to handled negative levels: // // slog.New(ToSlogHandler(logger)).Debug(...) -> logger.GetSink().Info(level=4, ...) // slog.New(ToSlogHandler(logger)).Warning(...) -> logger.GetSink().Info(level=0, ...) // slog.New(ToSlogHandler(logger)).Info(...) -> logger.GetSink().Info(level=0, ...) // slog.New(ToSlogHandler(logger.V(4))).Info(...) -> logger.GetSink().Info(level=4, ...) func ToSlogHandler(logger Logger) slog.Handler { if sink, ok := logger.GetSink().(*slogSink); ok && logger.GetV() == 0 { return sink.handler } handler := &slogHandler{sink: logger.GetSink(), levelBias: slog.Level(logger.GetV())} if slogSink, ok := handler.sink.(SlogSink); ok { handler.slogSink = slogSink } return handler } // SlogSink is an optional interface that a LogSink can implement to support // logging through the slog.Logger or slog.Handler APIs better. It then should // also support special slog values like slog.Group. When used as a // slog.Handler, the advantages are: // // - stack unwinding gets avoided in favor of logging the pre-recorded PC, // as intended by slog // - proper grouping of key/value pairs via WithGroup // - verbosity levels > slog.LevelInfo can be recorded // - less overhead // // Both APIs (Logger and slog.Logger/Handler) then are supported equally // well. Developers can pick whatever API suits them better and/or mix // packages which use either API in the same binary with a common logging // implementation. // // This interface is necessary because the type implementing the LogSink // interface cannot also implement the slog.Handler interface due to the // different prototype of the common Enabled method. // // An implementation could support both interfaces in two different types, but then // additional interfaces would be needed to convert between those types in FromSlogHandler // and ToSlogHandler. type SlogSink interface { LogSink Handle(ctx context.Context, record slog.Record) error WithAttrs(attrs []slog.Attr) SlogSink WithGroup(name string) SlogSink } golang-github-go-logr-logr-1.4.2/slogr/000077500000000000000000000000001470525074300177075ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/slogr/slogr.go000066400000000000000000000035271470525074300213730ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package slogr enables usage of a slog.Handler with logr.Logger as front-end // API and of a logr.LogSink through the slog.Handler and thus slog.Logger // APIs. // // See the README in the top-level [./logr] package for a discussion of // interoperability. // // Deprecated: use the main logr package instead. package slogr import ( "log/slog" "github.com/go-logr/logr" ) // NewLogr returns a logr.Logger which writes to the slog.Handler. // // Deprecated: use [logr.FromSlogHandler] instead. func NewLogr(handler slog.Handler) logr.Logger { return logr.FromSlogHandler(handler) } // NewSlogHandler returns a slog.Handler which writes to the same sink as the logr.Logger. // // Deprecated: use [logr.ToSlogHandler] instead. func NewSlogHandler(logger logr.Logger) slog.Handler { return logr.ToSlogHandler(logger) } // ToSlogHandler returns a slog.Handler which writes to the same sink as the logr.Logger. // // Deprecated: use [logr.ToSlogHandler] instead. func ToSlogHandler(logger logr.Logger) slog.Handler { return logr.ToSlogHandler(logger) } // SlogSink is an optional interface that a LogSink can implement to support // logging through the slog.Logger or slog.Handler APIs better. // // Deprecated: use [logr.SlogSink] instead. type SlogSink = logr.SlogSink golang-github-go-logr-logr-1.4.2/slogr_test.go000066400000000000000000000134331470525074300213010ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "bytes" "fmt" "io" "log/slog" "os" "path" "runtime" "strings" "testing" "github.com/go-logr/logr/internal/testhelp" ) func TestToSlogHandler(t *testing.T) { t.Run("from simple Logger", func(t *testing.T) { logger := New(&testLogSink{}) handler := ToSlogHandler(logger) if _, ok := handler.(*slogHandler); !ok { t.Errorf("expected type *slogHandler, got %T", handler) } }) t.Run("from slog-enabled Logger", func(t *testing.T) { logger := New(&testSlogSink{}) handler := ToSlogHandler(logger) if _, ok := handler.(*slogHandler); !ok { t.Errorf("expected type *slogHandler, got %T", handler) } }) t.Run("from slogSink Logger", func(t *testing.T) { logger := New(&slogSink{handler: slog.NewJSONHandler(os.Stderr, nil)}) handler := ToSlogHandler(logger) if _, ok := handler.(*slog.JSONHandler); !ok { t.Errorf("expected type *slog.JSONHandler, got %T", handler) } }) } func TestFromSlogHandler(t *testing.T) { t.Run("from slog Handler", func(t *testing.T) { handler := slog.NewJSONHandler(os.Stderr, nil) logger := FromSlogHandler(handler) if _, ok := logger.sink.(*slogSink); !ok { t.Errorf("expected type *slogSink, got %T", logger.sink) } }) t.Run("from simple slogHandler Handler", func(t *testing.T) { handler := &slogHandler{sink: &testLogSink{}} logger := FromSlogHandler(handler) if _, ok := logger.sink.(*testLogSink); !ok { t.Errorf("expected type *testSlogSink, got %T", logger.sink) } }) t.Run("from discard slogHandler Handler", func(t *testing.T) { handler := &slogHandler{} logger := FromSlogHandler(handler) if logger != Discard() { t.Errorf("expected type *testSlogSink, got %T", logger.sink) } }) } var debugWithoutTime = &slog.HandlerOptions{ ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { if a.Key == "time" { return slog.Attr{} } return a }, Level: slog.LevelDebug, } func TestWithCallDepth(t *testing.T) { debugWithCaller := *debugWithoutTime debugWithCaller.AddSource = true var buffer bytes.Buffer logger := FromSlogHandler(slog.NewTextHandler(&buffer, &debugWithCaller)) logHelper := func(logger Logger) { logger.WithCallDepth(1).Info("hello") } logHelper(logger) _, file, line, _ := runtime.Caller(0) expectedSource := fmt.Sprintf("%s:%d", path.Base(file), line-1) actual := buffer.String() if !strings.Contains(actual, expectedSource) { t.Errorf("expected log entry with %s as caller source code location, got instead:\n%s", expectedSource, actual) } } func TestRunSlogTestsOnSlogHandlerLogSink(t *testing.T) { // This proves that slogHandler passes slog's own tests when given a // non-SlogSink LogSink. exceptions := []string{ // logr sinks handle time themselves "a Handler should ignore a zero Record.Time", // slogHandler does not do groups "properly", so these all fail with // "missing group". It's looking for `"G":{"a":"b"}` and getting // `"G.a": "b"`. "a Handler should handle Group attributes", "a Handler should handle the WithGroup method", "a Handler should handle multiple WithGroup and WithAttr calls", "a Handler should not output groups for an empty Record", "a Handler should not output groups if there are no attributes", "a Handler should call Resolve on attribute values in groups", "a Handler should call Resolve on attribute values in groups from WithAttrs", } testhelp.RunSlogTests(t, func(buffer *bytes.Buffer) slog.Handler { // We want a known-good Logger that emits JSON but is not a slogHandler // or SlogSink (since those get special treatment). We can trust that // the slog JSONHandler works. handler := slog.NewJSONHandler(buffer, nil) sink := &passthruLogSink{handler: handler} logger := New(sink) return ToSlogHandler(logger) }, exceptions...) } func TestRunSlogTestsOnSlogHandlerSlogSink(t *testing.T) { // This proves that slogHandler passes slog's own tests when given a // SlogSink. exceptions := []string{} testhelp.RunSlogTests(t, func(buffer *bytes.Buffer) slog.Handler { // We want a known-good Logger that emits JSON and implements SlogSink, // to cover those paths. We can trust that the slog JSONHandler works. handler := slog.NewJSONHandler(buffer, nil) sink := &passthruSlogSink{handler: handler} logger := New(sink) return ToSlogHandler(logger) }, exceptions...) } func TestSlogSinkOnDiscard(_ *testing.T) { // Compile-test logger := slog.New(ToSlogHandler(Discard())) logger.WithGroup("foo").With("x", 1).Info("hello") } func TestConversion(t *testing.T) { d := Discard() d2 := FromSlogHandler(ToSlogHandler(d)) expectEqual(t, d, d2) e := Logger{} e2 := FromSlogHandler(ToSlogHandler(e)) expectEqual(t, e, e2) text := slog.NewTextHandler(io.Discard, nil) text2 := ToSlogHandler(FromSlogHandler(text)) expectEqual(t, text, text2) text3 := ToSlogHandler(FromSlogHandler(text).V(1)) if handler, ok := text3.(interface { GetLevel() slog.Level }); ok { expectEqual(t, handler.GetLevel(), slog.Level(1)) } else { t.Errorf("Expected a slogHandler which implements V(1), got instead: %T %+v", text3, text3) } } func expectEqual(t *testing.T, expected, actual any) { if expected != actual { t.Helper() t.Errorf("Expected %T %+v, got instead: %T %+v", expected, expected, actual, actual) } } golang-github-go-logr-logr-1.4.2/slogsink.go000066400000000000000000000056651470525074300207550ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "context" "log/slog" "runtime" "time" ) var ( _ LogSink = &slogSink{} _ CallDepthLogSink = &slogSink{} _ Underlier = &slogSink{} ) // Underlier is implemented by the LogSink returned by NewFromLogHandler. type Underlier interface { // GetUnderlying returns the Handler used by the LogSink. GetUnderlying() slog.Handler } const ( // nameKey is used to log the `WithName` values as an additional attribute. nameKey = "logger" // errKey is used to log the error parameter of Error as an additional attribute. errKey = "err" ) type slogSink struct { callDepth int name string handler slog.Handler } func (l *slogSink) Init(info RuntimeInfo) { l.callDepth = info.CallDepth } func (l *slogSink) GetUnderlying() slog.Handler { return l.handler } func (l *slogSink) WithCallDepth(depth int) LogSink { newLogger := *l newLogger.callDepth += depth return &newLogger } func (l *slogSink) Enabled(level int) bool { return l.handler.Enabled(context.Background(), slog.Level(-level)) } func (l *slogSink) Info(level int, msg string, kvList ...interface{}) { l.log(nil, msg, slog.Level(-level), kvList...) } func (l *slogSink) Error(err error, msg string, kvList ...interface{}) { l.log(err, msg, slog.LevelError, kvList...) } func (l *slogSink) log(err error, msg string, level slog.Level, kvList ...interface{}) { var pcs [1]uintptr // skip runtime.Callers, this function, Info/Error, and all helper functions above that. runtime.Callers(3+l.callDepth, pcs[:]) record := slog.NewRecord(time.Now(), level, msg, pcs[0]) if l.name != "" { record.AddAttrs(slog.String(nameKey, l.name)) } if err != nil { record.AddAttrs(slog.Any(errKey, err)) } record.Add(kvList...) _ = l.handler.Handle(context.Background(), record) } func (l slogSink) WithName(name string) LogSink { if l.name != "" { l.name += "/" } l.name += name return &l } func (l slogSink) WithValues(kvList ...interface{}) LogSink { l.handler = l.handler.WithAttrs(kvListToAttrs(kvList...)) return &l } func kvListToAttrs(kvList ...interface{}) []slog.Attr { // We don't need the record itself, only its Add method. record := slog.NewRecord(time.Time{}, 0, "", 0) record.Add(kvList...) attrs := make([]slog.Attr, 0, record.NumAttrs()) record.Attrs(func(attr slog.Attr) bool { attrs = append(attrs, attr) return true }) return attrs } golang-github-go-logr-logr-1.4.2/testimpls_slog_test.go000066400000000000000000000074441470525074300232300ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr import ( "context" "log/slog" "time" ) var _ SlogSink = &testSlogSink{} // testSlogSink is a trivial SlogSink implementation, just for testing, which // calls (optional) hooks on each method. type testSlogSink struct { // embed a plain LogSink testLogSink attrs []slog.Attr groups []string fnHandle func(ss *testSlogSink, ctx context.Context, record slog.Record) fnWithAttrs func(ss *testSlogSink, attrs []slog.Attr) fnWithGroup func(ss *testSlogSink, name string) } func (ss *testSlogSink) Handle(ctx context.Context, record slog.Record) error { if ss.fnHandle != nil { ss.fnHandle(ss, ctx, record) } return nil } func (ss *testSlogSink) WithAttrs(attrs []slog.Attr) SlogSink { if ss.fnWithAttrs != nil { ss.fnWithAttrs(ss, attrs) } out := *ss n := len(out.attrs) out.attrs = append(out.attrs[:n:n], attrs...) return &out } func (ss *testSlogSink) WithGroup(name string) SlogSink { if ss.fnWithGroup != nil { ss.fnWithGroup(ss, name) } out := *ss n := len(out.groups) out.groups = append(out.groups[:n:n], name) return &out } // passthruLogSink is a trivial LogSink implementation, which implements the // logr.LogSink methods in terms of a slog.Handler. type passthruLogSink struct { handler slog.Handler } func (pl passthruLogSink) Init(RuntimeInfo) {} func (pl passthruLogSink) Enabled(int) bool { return true } func (pl passthruLogSink) Error(_ error, msg string, kvList ...interface{}) { var record slog.Record record.Message = msg record.Level = slog.LevelError record.Time = time.Now() record.Add(kvList...) _ = pl.handler.Handle(context.Background(), record) } func (pl passthruLogSink) Info(_ int, msg string, kvList ...interface{}) { var record slog.Record record.Message = msg record.Level = slog.LevelInfo record.Time = time.Now() record.Add(kvList...) _ = pl.handler.Handle(context.Background(), record) } func (pl passthruLogSink) WithName(string) LogSink { return &pl } func (pl passthruLogSink) WithValues(kvList ...interface{}) LogSink { var values slog.Record values.Add(kvList...) var attrs []slog.Attr add := func(attr slog.Attr) bool { attrs = append(attrs, attr) return true } values.Attrs(add) pl.handler = pl.handler.WithAttrs(attrs) return &pl } // passthruSlogSink is a trivial SlogSink implementation, which stubs out the // logr.LogSink methods and passes Logr.SlogSink thru to a slog.Handler. type passthruSlogSink struct { handler slog.Handler } func (ps passthruSlogSink) Init(RuntimeInfo) {} func (ps passthruSlogSink) Enabled(int) bool { return true } func (ps passthruSlogSink) Error(error, string, ...interface{}) {} func (ps passthruSlogSink) Info(int, string, ...interface{}) {} func (ps passthruSlogSink) WithName(string) LogSink { return &ps } func (ps passthruSlogSink) WithValues(...interface{}) LogSink { return &ps } func (ps *passthruSlogSink) Handle(ctx context.Context, record slog.Record) error { return ps.handler.Handle(ctx, record) } func (ps passthruSlogSink) WithAttrs(attrs []slog.Attr) SlogSink { ps.handler = ps.handler.WithAttrs(attrs) return &ps } func (ps passthruSlogSink) WithGroup(name string) SlogSink { ps.handler = ps.handler.WithGroup(name) return &ps } golang-github-go-logr-logr-1.4.2/testimpls_test.go000066400000000000000000000041561470525074300222010ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logr // testLogSink is a trivial LogSink implementation, just for testing, which // calls (optional) hooks on each method. type testLogSink struct { fnInit func(ri RuntimeInfo) fnEnabled func(lvl int) bool fnInfo func(lvl int, msg string, kv ...any) fnError func(err error, msg string, kv ...any) fnWithValues func(kv ...any) fnWithName func(name string) withValues []any } var _ LogSink = &testLogSink{} func (ls *testLogSink) Init(ri RuntimeInfo) { if ls.fnInit != nil { ls.fnInit(ri) } } func (ls *testLogSink) Enabled(lvl int) bool { if ls.fnEnabled != nil { return ls.fnEnabled(lvl) } return false } func (ls *testLogSink) Info(lvl int, msg string, kv ...any) { if ls.fnInfo != nil { ls.fnInfo(lvl, msg, kv...) } } func (ls *testLogSink) Error(err error, msg string, kv ...any) { if ls.fnError != nil { ls.fnError(err, msg, kv...) } } func (ls *testLogSink) WithValues(kv ...any) LogSink { if ls.fnWithValues != nil { ls.fnWithValues(kv...) } out := *ls n := len(out.withValues) out.withValues = append(out.withValues[:n:n], kv...) return &out } func (ls *testLogSink) WithName(name string) LogSink { if ls.fnWithName != nil { ls.fnWithName(name) } out := *ls return &out } type testCallDepthLogSink struct { testLogSink callDepth int fnWithCallDepth func(depth int) } var _ CallDepthLogSink = &testCallDepthLogSink{} func (ls *testCallDepthLogSink) WithCallDepth(depth int) LogSink { if ls.fnWithCallDepth != nil { ls.fnWithCallDepth(depth) } out := *ls out.callDepth += depth return &out } golang-github-go-logr-logr-1.4.2/testing/000077500000000000000000000000001470525074300202365ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/testing/test.go000066400000000000000000000026771470525074300215600ustar00rootroot00000000000000/* Copyright 2019 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package testing provides support for using logr in tests. // Deprecated. See github.com/go-logr/logr/testr instead. package testing import "github.com/go-logr/logr/testr" // NewTestLogger returns a logr.Logger that prints through a testing.T object. // Deprecated. See github.com/go-logr/logr/testr.New instead. var NewTestLogger = testr.New // Options carries parameters which influence the way logs are generated. // Deprecated. See github.com/go-logr/logr/testr.Options instead. type Options = testr.Options // NewTestLoggerWithOptions returns a logr.Logger that prints through a testing.T object. // Deprecated. See github.com/go-logr/logr/testr.NewWithOptions instead. var NewTestLoggerWithOptions = testr.NewWithOptions // Underlier exposes access to the underlying testing.T instance. // Deprecated. See github.com/go-logr/logr/testr.Underlier instead. type Underlier = testr.Underlier golang-github-go-logr-logr-1.4.2/testing/test_test.go000066400000000000000000000023411470525074300226030ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package testing import ( "fmt" "testing" "github.com/go-logr/logr" ) func TestLogger(t *testing.T) { log := NewTestLogger(t) log.Info("info") log.V(0).Info("V(0).info") log.V(1).Info("v(1).info") log.Error(fmt.Errorf("error"), "error") log.WithName("testing").Info("with prefix") Helper(log, "hello world") log = NewTestLoggerWithOptions(t, Options{ LogTimestamp: true, Verbosity: 1, }) log.V(1).Info("v(1).info with options") } func Helper(log logr.Logger, msg string) { helper, log := log.WithCallStackHelper() helper() helper2(log, msg) } func helper2(log logr.Logger, msg string) { helper, log := log.WithCallStackHelper() helper() log.Info(msg) } golang-github-go-logr-logr-1.4.2/testr/000077500000000000000000000000001470525074300177225ustar00rootroot00000000000000golang-github-go-logr-logr-1.4.2/testr/testr.go000066400000000000000000000116621470525074300214200ustar00rootroot00000000000000/* Copyright 2019 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package testr provides support for using logr in tests. package testr import ( "testing" "github.com/go-logr/logr" "github.com/go-logr/logr/funcr" ) // New returns a logr.Logger that prints through a testing.T object. // Info logs are only enabled at V(0). func New(t *testing.T) logr.Logger { return NewWithOptions(t, Options{}) } // Options carries parameters which influence the way logs are generated. type Options struct { // LogTimestamp tells the logger to add a "ts" key to log // lines. This has some overhead, so some users might not want // it. LogTimestamp bool // Verbosity tells the logger which V logs to be write. // Higher values enable more logs. Verbosity int } // NewWithOptions returns a logr.Logger that prints through a testing.T object. // In contrast to the simpler New, output formatting can be configured. func NewWithOptions(t *testing.T, opts Options) logr.Logger { l := &testlogger{ testloggerInterface: newLoggerInterfaceWithOptions(t, opts), } return logr.New(l) } // TestingT is an interface wrapper around testing.T, testing.B and testing.F. type TestingT interface { Helper() Log(args ...any) } // NewWithInterface returns a logr.Logger that prints through a // TestingT object. // In contrast to the simpler New, output formatting can be configured. func NewWithInterface(t TestingT, opts Options) logr.Logger { l := newLoggerInterfaceWithOptions(t, opts) return logr.New(&l) } func newLoggerInterfaceWithOptions(t TestingT, opts Options) testloggerInterface { return testloggerInterface{ t: t, Formatter: funcr.NewFormatter(funcr.Options{ LogTimestamp: opts.LogTimestamp, Verbosity: opts.Verbosity, }), } } // Underlier exposes access to the underlying testing.T instance. Since // callers only have a logr.Logger, they have to know which // implementation is in use, so this interface is less of an // abstraction and more of a way to test type conversion. type Underlier interface { GetUnderlying() *testing.T } // UnderlierInterface exposes access to the underlying TestingT instance. Since // callers only have a logr.Logger, they have to know which // implementation is in use, so this interface is less of an // abstraction and more of a way to test type conversion. type UnderlierInterface interface { GetUnderlying() TestingT } // Info logging implementation shared between testLogger and testLoggerInterface. func logInfo(t TestingT, formatInfo func(int, string, []any) (string, string), level int, msg string, kvList ...any) { prefix, args := formatInfo(level, msg, kvList) t.Helper() if prefix != "" { args = prefix + ": " + args } t.Log(args) } // Error logging implementation shared between testLogger and testLoggerInterface. func logError(t TestingT, formatError func(error, string, []any) (string, string), err error, msg string, kvList ...any) { prefix, args := formatError(err, msg, kvList) t.Helper() if prefix != "" { args = prefix + ": " + args } t.Log(args) } // This type exists to wrap and modify the method-set of testloggerInterface. // In particular, it changes the GetUnderlying() method. type testlogger struct { testloggerInterface } func (l testlogger) GetUnderlying() *testing.T { // This method is defined on testlogger, so the only type this could // possibly be is testing.T, even though that's not guaranteed by the type // system itself. return l.t.(*testing.T) //nolint:forcetypeassert } type testloggerInterface struct { funcr.Formatter t TestingT } func (l testloggerInterface) WithName(name string) logr.LogSink { l.Formatter.AddName(name) return &l } func (l testloggerInterface) WithValues(kvList ...any) logr.LogSink { l.Formatter.AddValues(kvList) return &l } func (l testloggerInterface) GetCallStackHelper() func() { return l.t.Helper } func (l testloggerInterface) Info(level int, msg string, kvList ...any) { l.t.Helper() logInfo(l.t, l.FormatInfo, level, msg, kvList...) } func (l testloggerInterface) Error(err error, msg string, kvList ...any) { l.t.Helper() logError(l.t, l.FormatError, err, msg, kvList...) } func (l testloggerInterface) GetUnderlying() TestingT { return l.t } // Assert conformance to the interfaces. var _ logr.LogSink = &testlogger{} var _ logr.CallStackHelperLogSink = &testlogger{} var _ Underlier = &testlogger{} var _ logr.LogSink = &testloggerInterface{} var _ logr.CallStackHelperLogSink = &testloggerInterface{} var _ UnderlierInterface = &testloggerInterface{} golang-github-go-logr-logr-1.4.2/testr/testr_fuzz_test.go000066400000000000000000000013331470525074300235270ustar00rootroot00000000000000//go:build go1.18 // +build go1.18 /* Copyright 2022 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package testr import "testing" func TestLoggerTestingF(_ *testing.T) { f := &testing.F{} _ = NewWithInterface(f, Options{}) } golang-github-go-logr-logr-1.4.2/testr/testr_test.go000066400000000000000000000042651470525074300224600ustar00rootroot00000000000000/* Copyright 2021 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package testr import ( "fmt" "testing" "github.com/go-logr/logr" ) func TestLogger(t *testing.T) { log := New(t) log.Info("info") log.V(0).Info("V(0).info") log.V(1).Info("v(1).info") log.Error(fmt.Errorf("error"), "error") log.WithName("testing").WithValues("value", "test").Info("with prefix") log.WithName("testing").Error(fmt.Errorf("error"), "with prefix") Helper(log, "hello world") log = NewWithOptions(t, Options{ LogTimestamp: true, Verbosity: 1, }) log.V(1).Info("v(1).info with options") underlier, ok := log.GetSink().(Underlier) if !ok { t.Fatal("couldn't get underlier") } if t != underlier.GetUnderlying() { t.Error("invalid underlier") } } func TestLoggerInterface(t *testing.T) { log := NewWithInterface(t, Options{}) log.Info("info") log.V(0).Info("V(0).info") log.V(1).Info("v(1).info") log.Error(fmt.Errorf("error"), "error") log.WithName("testing").WithValues("value", "test").Info("with prefix") log.WithName("testing").Error(fmt.Errorf("error"), "with prefix") Helper(log, "hello world") underlier, ok := log.GetSink().(UnderlierInterface) if !ok { t.Fatal("couldn't get underlier") } underlierT, ok := underlier.GetUnderlying().(*testing.T) if !ok { t.Fatal("couldn't get underlying *testing.T") } if t != underlierT { t.Error("invalid underlier") } } func TestLoggerTestingB(_ *testing.T) { b := &testing.B{} _ = NewWithInterface(b, Options{}) } func Helper(log logr.Logger, msg string) { helper, log := log.WithCallStackHelper() helper() helper2(log, msg) } func helper2(log logr.Logger, msg string) { helper, log := log.WithCallStackHelper() helper() log.Info(msg) }