├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .golangci.yaml ├── CONTRIBUTING.md ├── LICENSE ├── OWNERS ├── README.md ├── RELEASE.md ├── SECURITY.md ├── SECURITY_CONTACTS ├── code-of-conduct.md ├── contextual.go ├── contextual_slog.go ├── contextual_slog_example_test.go ├── contextual_test.go ├── examples ├── benchmarks │ └── benchmarks_test.go ├── coexist_glog │ └── coexist_glog.go ├── coexist_klog_v1_and_v2 │ ├── coexist_klog_v1_and_v2.go │ └── go.mod ├── flushing │ ├── .gitignore │ └── flushing_test.go ├── go.mod ├── go.sum ├── go_vet │ ├── go_vet_test.go │ └── testdata │ │ └── calls.go ├── klogr │ └── main.go ├── log_file │ └── usage_log_file.go ├── output_test │ └── output_test.go ├── set_output │ └── usage_set_output.go ├── structured_logging │ └── structured_logging.go └── util │ └── require │ └── require.go ├── exit.go ├── exit_test.go ├── format.go ├── format_test.go ├── go.mod ├── go.sum ├── hack └── verify-apidiff.sh ├── imports.go ├── integration_tests ├── internal │ └── main.go └── klog_test.go ├── internal ├── buffer │ └── buffer.go ├── clock │ ├── README.md │ ├── clock.go │ └── testing │ │ ├── fake_clock.go │ │ └── simple_interval_clock.go ├── dbg │ └── dbg.go ├── serialize │ ├── keyvalues.go │ ├── keyvalues_no_slog.go │ ├── keyvalues_slog.go │ └── keyvalues_test.go ├── severity │ └── severity.go ├── sloghandler │ └── sloghandler_slog.go ├── test │ ├── mock.go │ └── require │ │ └── require.go └── verbosity │ ├── helper_test.go │ ├── verbosity.go │ └── verbosity_test.go ├── k8s_references.go ├── k8s_references_slog.go ├── klog.go ├── klog_file.go ├── klog_file_others.go ├── klog_file_windows.go ├── klog_test.go ├── klog_wrappers_test.go ├── klogr.go ├── klogr ├── README.md ├── calldepth-test │ ├── call_depth_helper_test.go │ └── call_depth_main_test.go ├── klogr.go ├── klogr_test.go └── output_test.go ├── klogr_helper_test.go ├── klogr_slog.go ├── klogr_slog_test.go ├── klogr_test.go ├── ktesting ├── contextual_test.go ├── example │ └── example_test.go ├── example_test.go ├── init │ └── init.go ├── options.go ├── setup.go ├── testinglogger.go └── testinglogger_test.go ├── output_test.go ├── safeptr.go ├── safeptr_test.go ├── test ├── output.go ├── output_helper.go └── zapr.go └── textlogger ├── example_test.go ├── options.go ├── output_test.go ├── textlogger.go ├── textlogger_slog.go ├── textlogger_slog_test.go └── textlogger_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Tell us about a problem you are experiencing 4 | 5 | --- 6 | 7 | /kind bug 8 | 9 | **What steps did you take and what happened:** 10 | [A clear and concise description of what the bug is.] 11 | 12 | 13 | **What did you expect to happen:** 14 | 15 | 16 | **Anything else you would like to add:** 17 | [Miscellaneous information that will assist in solving the issue.] 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature enhancement request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | /kind feature 8 | 9 | **Describe the solution you'd like** 10 | [A clear and concise description of what you want to happen.] 11 | 12 | 13 | **Anything else you would like to add:** 14 | [Miscellaneous information that will assist in solving the issue.] 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | **What this PR does / why we need it**: 10 | 11 | **Which issue(s) this PR fixes** *(optional, in `fixes #(, fixes #, ...)` format, will close the issue(s) when PR gets merged)*: 12 | Fixes # 13 | 14 | **Special notes for your reviewer**: 15 | 16 | _Please confirm that if this PR changes any image versions, then that's the sole change this PR makes._ 17 | 18 | **Release note**: 19 | 23 | ```release-note 24 | 25 | ``` -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directories: 5 | - "**/*" 6 | schedule: 7 | interval: daily 8 | 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: daily 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Run lint 2 | 3 | on: [ push, pull_request ] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | lint: 10 | strategy: 11 | matrix: 12 | path: 13 | - . 14 | - examples 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | - name: Lint 20 | uses: golangci/golangci-lint-action@v6 21 | with: 22 | # version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 23 | version: latest 24 | working-directory: ${{ matrix.path }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: ["1.21", "1.22", "1.23"] 8 | platform: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | - name: Test klog 18 | run: | 19 | go get -t -v ./... 20 | go test -v -race ./... 21 | - name: Test examples 22 | run: cd examples && go test -v -race ./... 23 | apidiff: 24 | runs-on: ubuntu-latest 25 | if: github.base_ref 26 | steps: 27 | - name: Install Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version: 1.23 31 | - name: Add GOBIN to PATH 32 | run: echo "PATH=$(go env GOPATH)/bin:$PATH" >>$GITHUB_ENV 33 | - name: Install dependencies 34 | run: go install golang.org/x/exp/cmd/apidiff@latest 35 | - name: Checkout old code 36 | uses: actions/checkout@v4 37 | with: 38 | ref: ${{ github.base_ref }} 39 | path: "old" 40 | - name: Checkout new code 41 | uses: actions/checkout@v4 42 | with: 43 | path: "new" 44 | - name: APIDiff 45 | run: ./hack/verify-apidiff.sh -d ../old 46 | working-directory: "new" 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX leaves these everywhere on SMB shares 2 | ._* 3 | 4 | # OSX trash 5 | .DS_Store 6 | 7 | # Eclipse files 8 | .classpath 9 | .project 10 | .settings/** 11 | 12 | # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA 13 | .idea/ 14 | *.iml 15 | 16 | # Vscode files 17 | .vscode 18 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: # sorted alphabetical 4 | - gofmt 5 | - misspell 6 | - revive 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://github.com/kubernetes/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: 4 | 5 | _As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ 6 | 7 | ## Getting Started 8 | 9 | We have full documentation on how to get started contributing here: 10 | 11 | - [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests 12 | - [Kubernetes Contributor Guide](http://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](http://git.k8s.io/community/contributors/guide#contributing) 13 | - [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet) - Common resources for existing developers 14 | 15 | ## Mentorship 16 | 17 | - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! 18 | 19 | ## Contact Information 20 | 21 | - [Slack](https://kubernetes.slack.com/messages/sig-architecture) 22 | - [Mailing List](https://groups.google.com/forum/#!forum/kubernetes-sig-architecture) 23 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | reviewers: 3 | - harshanarayana 4 | - mengjiao-liu 5 | - pohly 6 | approvers: 7 | - dims 8 | - pohly 9 | - thockin 10 | emeritus_approvers: 11 | - brancz 12 | - justinsb 13 | - lavalamp 14 | - piosz 15 | - serathius 16 | - tallclair 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | klog 2 | ==== 3 | 4 | klog is a permanent fork of https://github.com/golang/glog. 5 | 6 | ## Why was klog created? 7 | 8 | The decision to create klog was one that wasn't made lightly, but it was necessary due to some 9 | drawbacks that are present in [glog](https://github.com/golang/glog). Ultimately, the fork was created due to glog not being under active development; this can be seen in the glog README: 10 | 11 | > The code in this repo [...] is not itself under development 12 | 13 | This makes us unable to solve many use cases without a fork. The factors that contributed to needing feature development are listed below: 14 | 15 | * `glog` [presents a lot "gotchas"](https://github.com/kubernetes/kubernetes/issues/61006) and introduces challenges in containerized environments, all of which aren't well documented. 16 | * `glog` doesn't provide an easy way to test logs, which detracts from the stability of software using it 17 | * A long term goal is to implement a logging interface that allows us to add context, change output format, etc. 18 | 19 | Historical context is available here: 20 | 21 | * https://github.com/kubernetes/kubernetes/issues/61006 22 | * https://github.com/kubernetes/kubernetes/issues/70264 23 | * https://groups.google.com/forum/#!msg/kubernetes-sig-architecture/wCWiWf3Juzs/hXRVBH90CgAJ 24 | * https://groups.google.com/forum/#!msg/kubernetes-dev/7vnijOMhLS0/1oRiNtigBgAJ 25 | 26 | ## Release versioning 27 | 28 | Semantic versioning is used in this repository. It contains several Go modules 29 | with different levels of stability: 30 | - `k8s.io/klog/v2` - stable API, `vX.Y.Z` tags 31 | - `examples` - no stable API, no tags, no intention to ever stabilize 32 | 33 | Exempt from the API stability guarantee are items (packages, functions, etc.) 34 | which are marked explicitly as `EXPERIMENTAL` in their docs comment. Those 35 | may still change in incompatible ways or get removed entirely. This can only 36 | be used for code that is used in tests to avoid situations where non-test 37 | code from two different Kubernetes dependencies depends on incompatible 38 | releases of klog because an experimental API was changed. 39 | 40 | ---- 41 | 42 | How to use klog 43 | =============== 44 | - Replace imports for `"github.com/golang/glog"` with `"k8s.io/klog/v2"` 45 | - Use `klog.InitFlags(nil)` explicitly for initializing global flags as we no longer use `init()` method to register the flags 46 | - You can now use `log_file` instead of `log_dir` for logging to a single file (See `examples/log_file/usage_log_file.go`) 47 | - If you want to redirect everything logged using klog somewhere else (say syslog!), you can use `klog.SetOutput()` method and supply a `io.Writer`. (See `examples/set_output/usage_set_output.go`) 48 | - For more logging conventions (See [Logging Conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md)) 49 | - See our documentation on [pkg.go.dev/k8s.io](https://pkg.go.dev/k8s.io/klog). 50 | 51 | **NOTE**: please use the newer go versions that support semantic import versioning in modules, ideally go 1.11.4 or greater. 52 | 53 | ### Coexisting with klog/v2 54 | 55 | See [this example](examples/coexist_klog_v1_and_v2/) to see how to coexist with both klog/v1 and klog/v2. 56 | 57 | ### Coexisting with glog 58 | This package can be used side by side with glog. [This example](examples/coexist_glog/coexist_glog.go) shows how to initialize and synchronize flags from the global `flag.CommandLine` FlagSet. In addition, the example makes use of stderr as combined output by setting `alsologtostderr` (or `logtostderr`) to `true`. 59 | 60 | ## Community, discussion, contribution, and support 61 | 62 | Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). 63 | 64 | You can reach the maintainers of this project at: 65 | 66 | - [Slack](https://kubernetes.slack.com/messages/klog) 67 | - [Mailing List](https://groups.google.com/forum/#!forum/kubernetes-sig-architecture) 68 | 69 | ### Code of conduct 70 | 71 | Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). 72 | 73 | ---- 74 | 75 | glog 76 | ==== 77 | 78 | Leveled execution logs for Go. 79 | 80 | This is an efficient pure Go implementation of leveled logs in the 81 | manner of the open source C++ package 82 | https://github.com/google/glog 83 | 84 | By binding methods to booleans it is possible to use the log package 85 | without paying the expense of evaluating the arguments to the log. 86 | Through the -vmodule flag, the package also provides fine-grained 87 | control over logging at the file level. 88 | 89 | The comment from glog.go introduces the ideas: 90 | 91 | Package glog implements logging analogous to the Google-internal 92 | C++ INFO/ERROR/V setup. It provides functions Info, Warning, 93 | Error, Fatal, plus formatting variants such as Infof. It 94 | also provides V-style logging controlled by the -v and 95 | -vmodule=file=2 flags. 96 | 97 | Basic examples: 98 | 99 | glog.Info("Prepare to repel boarders") 100 | 101 | glog.Fatalf("Initialization failed: %s", err) 102 | 103 | See the documentation of the V function for an explanation 104 | of these examples: 105 | 106 | if glog.V(2) { 107 | glog.Info("Starting transaction...") 108 | } 109 | 110 | glog.V(2).Infoln("Processed", nItems, "elements") 111 | 112 | 113 | The repository contains an open source version of the log package 114 | used inside Google. The master copy of the source lives inside 115 | Google, not here. The code in this repo is for export only and is not itself 116 | under development. Feature requests will be ignored. 117 | 118 | Send bug reports to golang-nuts@googlegroups.com. 119 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | The `klog` is released on an as-needed basis. The process is as follows: 4 | 5 | 1. An issue is proposing a new release with a changelog since the last release 6 | 1. All [OWNERS](OWNERS) must LGTM this release 7 | 1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION` 8 | 1. The release issue is closed 9 | 1. An announcement email is sent to `kubernetes-dev@googlegroups.com` with the subject `[ANNOUNCE] kubernetes-template-project $VERSION is released` 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security Announcements 4 | 5 | Join the [kubernetes-security-announce] group for security and vulnerability announcements. 6 | 7 | You can also subscribe to an RSS feed of the above using [this link][kubernetes-security-announce-rss]. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Instructions for reporting a vulnerability can be found on the 12 | [Kubernetes Security and Disclosure Information] page. 13 | 14 | ## Supported Versions 15 | 16 | Information about supported Kubernetes versions can be found on the 17 | [Kubernetes version and version skew support policy] page on the Kubernetes website. 18 | 19 | [kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce 20 | [kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50 21 | [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions 22 | [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability 23 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Committee to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | dims 14 | thockin 15 | justinsb 16 | tallclair 17 | piosz 18 | brancz 19 | DirectXMan12 20 | lavalamp 21 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /contextual.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klog 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/go-logr/logr" 23 | ) 24 | 25 | // This file provides the implementation of 26 | // https://github.com/kubernetes/enhancements/tree/master/keps/sig-instrumentation/1602-structured-logging 27 | // 28 | // SetLogger and ClearLogger were originally added to klog.go and got moved 29 | // here. Contextual logging adds a way to retrieve a Logger for direct logging 30 | // without the logging calls in klog.go. 31 | // 32 | // The global variables are expected to be modified only during sequential 33 | // parts of a program (init, serial tests) and therefore are not protected by 34 | // mutex locking. 35 | 36 | var ( 37 | // klogLogger is used as fallback for logging through the normal klog code 38 | // when no Logger is set. 39 | klogLogger logr.Logger = logr.New(&klogger{}) 40 | ) 41 | 42 | // SetLogger sets a Logger implementation that will be used as backing 43 | // implementation of the traditional klog log calls. klog will do its own 44 | // verbosity checks before calling logger.V().Info. logger.Error is always 45 | // called, regardless of the klog verbosity settings. 46 | // 47 | // If set, all log lines will be suppressed from the regular output, and 48 | // redirected to the logr implementation. 49 | // Use as: 50 | // 51 | // ... 52 | // klog.SetLogger(zapr.NewLogger(zapLog)) 53 | // 54 | // To remove a backing logr implemention, use ClearLogger. Setting an 55 | // empty logger with SetLogger(logr.Logger{}) does not work. 56 | // 57 | // Modifying the logger is not thread-safe and should be done while no other 58 | // goroutines invoke log calls, usually during program initialization. 59 | func SetLogger(logger logr.Logger) { 60 | SetLoggerWithOptions(logger) 61 | } 62 | 63 | // SetLoggerWithOptions is a more flexible version of SetLogger. Without 64 | // additional options, it behaves exactly like SetLogger. By passing 65 | // ContextualLogger(true) as option, it can be used to set a logger that then 66 | // will also get called directly by applications which retrieve it via 67 | // FromContext, Background, or TODO. 68 | // 69 | // Supporting direct calls is recommended because it avoids the overhead of 70 | // routing log entries through klogr into klog and then into the actual Logger 71 | // backend. 72 | func SetLoggerWithOptions(logger logr.Logger, opts ...LoggerOption) { 73 | logging.loggerOptions = loggerOptions{} 74 | for _, opt := range opts { 75 | opt(&logging.loggerOptions) 76 | } 77 | logging.logger = &logWriter{ 78 | Logger: logger, 79 | writeKlogBuffer: logging.loggerOptions.writeKlogBuffer, 80 | } 81 | } 82 | 83 | // ContextualLogger determines whether the logger passed to 84 | // SetLoggerWithOptions may also get called directly. Such a logger cannot rely 85 | // on verbosity checking in klog. 86 | func ContextualLogger(enabled bool) LoggerOption { 87 | return func(o *loggerOptions) { 88 | o.contextualLogger = enabled 89 | } 90 | } 91 | 92 | // FlushLogger provides a callback for flushing data buffered by the logger. 93 | func FlushLogger(flush func()) LoggerOption { 94 | return func(o *loggerOptions) { 95 | o.flush = flush 96 | } 97 | } 98 | 99 | // WriteKlogBuffer sets a callback that will be invoked by klog to write output 100 | // produced by non-structured log calls like Infof. 101 | // 102 | // The buffer will contain exactly the same data that klog normally would write 103 | // into its own output stream(s). In particular this includes the header, if 104 | // klog is configured to write one. The callback then can divert that data into 105 | // its own output streams. The buffer may or may not end in a line break. 106 | // 107 | // Without such a callback, klog will call the logger's Info or Error method 108 | // with just the message string (i.e. no header). 109 | func WriteKlogBuffer(write func([]byte)) LoggerOption { 110 | return func(o *loggerOptions) { 111 | o.writeKlogBuffer = write 112 | } 113 | } 114 | 115 | // LoggerOption implements the functional parameter paradigm for 116 | // SetLoggerWithOptions. 117 | type LoggerOption func(o *loggerOptions) 118 | 119 | type loggerOptions struct { 120 | contextualLogger bool 121 | flush func() 122 | writeKlogBuffer func([]byte) 123 | } 124 | 125 | // logWriter combines a logger (always set) with a write callback (optional). 126 | type logWriter struct { 127 | Logger 128 | writeKlogBuffer func([]byte) 129 | } 130 | 131 | // ClearLogger removes a backing Logger implementation if one was set earlier 132 | // with SetLogger. 133 | // 134 | // Modifying the logger is not thread-safe and should be done while no other 135 | // goroutines invoke log calls, usually during program initialization. 136 | func ClearLogger() { 137 | logging.logger = nil 138 | logging.loggerOptions = loggerOptions{} 139 | } 140 | 141 | // EnableContextualLogging controls whether contextual logging is enabled. 142 | // By default it is enabled. When disabled, FromContext avoids looking up 143 | // the logger in the context and always returns the global logger. 144 | // LoggerWithValues, LoggerWithName, and NewContext become no-ops 145 | // and return their input logger respectively context. This may be useful 146 | // to avoid the additional overhead for contextual logging. 147 | // 148 | // This must be called during initialization before goroutines are started. 149 | func EnableContextualLogging(enabled bool) { 150 | logging.contextualLoggingEnabled = enabled 151 | } 152 | 153 | // FromContext retrieves a logger set by the caller or, if not set, 154 | // falls back to the program's global logger (a Logger instance or klog 155 | // itself). 156 | func FromContext(ctx context.Context) Logger { 157 | if logging.contextualLoggingEnabled { 158 | if logger, err := logr.FromContext(ctx); err == nil { 159 | return logger 160 | } 161 | } 162 | 163 | return Background() 164 | } 165 | 166 | // TODO can be used as a last resort by code that has no means of 167 | // receiving a logger from its caller. FromContext or an explicit logger 168 | // parameter should be used instead. 169 | func TODO() Logger { 170 | return Background() 171 | } 172 | 173 | // Background retrieves the fallback logger. It should not be called before 174 | // that logger was initialized by the program and not by code that should 175 | // better receive a logger via its parameters. TODO can be used as a temporary 176 | // solution for such code. 177 | func Background() Logger { 178 | if logging.loggerOptions.contextualLogger { 179 | // Is non-nil because logging.loggerOptions.contextualLogger is 180 | // only true if a logger was set. 181 | return logging.logger.Logger 182 | } 183 | 184 | return klogLogger 185 | } 186 | 187 | // LoggerWithValues returns logger.WithValues(...kv) when 188 | // contextual logging is enabled, otherwise the logger. 189 | func LoggerWithValues(logger Logger, kv ...interface{}) Logger { 190 | if logging.contextualLoggingEnabled { 191 | return logger.WithValues(kv...) 192 | } 193 | return logger 194 | } 195 | 196 | // LoggerWithName returns logger.WithName(name) when contextual logging is 197 | // enabled, otherwise the logger. 198 | func LoggerWithName(logger Logger, name string) Logger { 199 | if logging.contextualLoggingEnabled { 200 | return logger.WithName(name) 201 | } 202 | return logger 203 | } 204 | 205 | // NewContext returns logr.NewContext(ctx, logger) when 206 | // contextual logging is enabled, otherwise ctx. 207 | func NewContext(ctx context.Context, logger Logger) context.Context { 208 | if logging.contextualLoggingEnabled { 209 | return logr.NewContext(ctx, logger) 210 | } 211 | return ctx 212 | } 213 | -------------------------------------------------------------------------------- /contextual_slog.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2021 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package klog 21 | 22 | import ( 23 | "log/slog" 24 | 25 | "github.com/go-logr/logr" 26 | ) 27 | 28 | // SetSlogLogger reconfigures klog to log through the slog logger. The logger must not be nil. 29 | func SetSlogLogger(logger *slog.Logger) { 30 | SetLoggerWithOptions(logr.FromSlogHandler(logger.Handler()), ContextualLogger(true)) 31 | } 32 | -------------------------------------------------------------------------------- /contextual_slog_example_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2021 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package klog_test 21 | 22 | import ( 23 | "log/slog" 24 | "os" 25 | 26 | "k8s.io/klog/v2" 27 | ) 28 | 29 | func ExampleSetSlogLogger() { 30 | state := klog.CaptureState() 31 | defer state.Restore() 32 | 33 | handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 34 | ReplaceAttr: func(_ /* groups */ []string, a slog.Attr) slog.Attr { 35 | if a.Key == slog.TimeKey { 36 | // Avoid non-deterministic output. 37 | return slog.Attr{} 38 | } 39 | return a 40 | }, 41 | }) 42 | logger := slog.New(handler) 43 | klog.SetSlogLogger(logger) 44 | klog.Info("hello world") 45 | 46 | // Output: 47 | // level=INFO msg="hello world" 48 | } 49 | -------------------------------------------------------------------------------- /contextual_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klog_test 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "runtime" 23 | "testing" 24 | 25 | "github.com/go-logr/logr" 26 | "k8s.io/klog/v2" 27 | ) 28 | 29 | func ExampleSetLogger() { 30 | defer klog.ClearLogger() 31 | 32 | // Logger is only used as backend, Background() returns klogr. 33 | klog.SetLogger(logr.Discard()) 34 | fmt.Printf("logger after SetLogger: %T\n", klog.Background().GetSink()) 35 | 36 | // Logger is only used as backend, Background() returns klogr. 37 | klog.SetLoggerWithOptions(logr.Discard(), klog.ContextualLogger(false)) 38 | fmt.Printf("logger after SetLoggerWithOptions with ContextualLogger(false): %T\n", klog.Background().GetSink()) 39 | 40 | // Logger is used as backend and directly. 41 | klog.SetLoggerWithOptions(logr.Discard(), klog.ContextualLogger(true)) 42 | fmt.Printf("logger after SetLoggerWithOptions with ContextualLogger(true): %T\n", klog.Background().GetSink()) 43 | 44 | // Output: 45 | // logger after SetLogger: *klog.klogger 46 | // logger after SetLoggerWithOptions with ContextualLogger(false): *klog.klogger 47 | // logger after SetLoggerWithOptions with ContextualLogger(true): 48 | } 49 | 50 | func ExampleFlushLogger() { 51 | defer klog.ClearLogger() 52 | 53 | // This simple logger doesn't need flushing, but others might. 54 | klog.SetLoggerWithOptions(logr.Discard(), klog.FlushLogger(func() { 55 | fmt.Print("flushing...") 56 | })) 57 | klog.Flush() 58 | 59 | // Output: 60 | // flushing... 61 | } 62 | 63 | func BenchmarkPassingLogger(b *testing.B) { 64 | b.Run("with context", func(b *testing.B) { 65 | ctx := klog.NewContext(context.Background(), klog.Background()) 66 | var finalCtx context.Context 67 | for n := b.N; n > 0; n-- { 68 | finalCtx = passCtx(ctx) 69 | } 70 | runtime.KeepAlive(finalCtx) 71 | }) 72 | 73 | b.Run("without context", func(b *testing.B) { 74 | logger := klog.Background() 75 | var finalLogger klog.Logger 76 | for n := b.N; n > 0; n-- { 77 | finalLogger = passLogger(logger) 78 | } 79 | runtime.KeepAlive(finalLogger) 80 | }) 81 | } 82 | 83 | func BenchmarkExtractLogger(b *testing.B) { 84 | b.Run("from context", func(b *testing.B) { 85 | ctx := klog.NewContext(context.Background(), klog.Background()) 86 | var finalLogger klog.Logger 87 | for n := b.N; n > 0; n-- { 88 | finalLogger = extractCtx(ctx) 89 | } 90 | runtime.KeepAlive(finalLogger) 91 | }) 92 | } 93 | 94 | //go:noinline 95 | func passCtx(ctx context.Context) context.Context { return ctx } 96 | 97 | //go:noinline 98 | func extractCtx(ctx context.Context) klog.Logger { return klog.FromContext(ctx) } 99 | 100 | //go:noinline 101 | func passLogger(logger klog.Logger) klog.Logger { return logger } 102 | -------------------------------------------------------------------------------- /examples/benchmarks/benchmarks_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package benchmarks 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "io" 23 | "testing" 24 | 25 | "github.com/go-logr/logr" 26 | "github.com/go-logr/zapr" 27 | "go.uber.org/zap" 28 | "go.uber.org/zap/zapcore" 29 | 30 | "k8s.io/klog/examples/util/require" 31 | "k8s.io/klog/v2" 32 | ) 33 | 34 | const ( 35 | verbosityThreshold = 10 36 | ) 37 | 38 | func init() { 39 | // klog gets configured so that it writes to a single output file that 40 | // will be set during tests with SetOutput. 41 | klog.InitFlags(nil) 42 | require.NoError(flag.Set("v", fmt.Sprintf("%d", verbosityThreshold))) 43 | require.NoError(flag.Set("log_file", "/dev/null")) 44 | require.NoError(flag.Set("logtostderr", "false")) 45 | require.NoError(flag.Set("alsologtostderr", "false")) 46 | require.NoError(flag.Set("stderrthreshold", "10")) 47 | } 48 | 49 | type testcase struct { 50 | name string 51 | generate func() interface{} 52 | } 53 | 54 | func BenchmarkOutput(b *testing.B) { 55 | // We'll run each benchmark for different output formatting. 56 | configs := map[string]struct { 57 | init, cleanup func() 58 | }{ 59 | "klog": { 60 | init: func() { klog.SetOutput(discard{}) }, 61 | }, 62 | "zapr": { 63 | init: func() { klog.SetLogger(newZaprLogger()) }, 64 | cleanup: func() { klog.ClearLogger() }, 65 | }, 66 | } 67 | 68 | // Each benchmark tests formatting of one key/value pair, with 69 | // different values. The order is relevant here. 70 | var tests []testcase 71 | for length := 0; length <= 100; length += 10 { 72 | arg := make([]interface{}, length) 73 | for i := 0; i < length; i++ { 74 | arg[i] = KMetadataMock{Name: "a", NS: "a"} 75 | } 76 | tests = append(tests, testcase{ 77 | name: fmt.Sprintf("objects/%d", length), 78 | generate: func() interface{} { 79 | return klog.KObjSlice(arg) 80 | }, 81 | }) 82 | } 83 | 84 | // Verbosity checks may influence the result. 85 | verbosity := map[string]func(value interface{}){ 86 | "no-verbosity-check": func(value interface{}) { 87 | klog.InfoS("test", "key", value) 88 | }, 89 | "pass-verbosity-check": func(value interface{}) { 90 | klog.V(verbosityThreshold).InfoS("test", "key", value) 91 | }, 92 | "fail-verbosity-check": func(value interface{}) { 93 | klog.V(verbosityThreshold+1).InfoS("test", "key", value) 94 | }, 95 | "non-standard-int-key-check": func(value interface{}) { 96 | klog.InfoS("test", 1, value) 97 | }, 98 | "non-standard-struct-key-check": func(value interface{}) { 99 | klog.InfoS("test", struct{ key string }{"test"}, value) 100 | }, 101 | "non-standard-map-key-check": func(value interface{}) { 102 | klog.InfoS("test", map[string]bool{"key": true}, value) 103 | }, 104 | "pass-verbosity-non-standard-int-key-check": func(value interface{}) { 105 | klog.V(verbosityThreshold).InfoS("test", 1, value) 106 | }, 107 | "pass-verbosity-non-standard-struct-key-check": func(value interface{}) { 108 | klog.V(verbosityThreshold).InfoS("test", struct{ key string }{"test"}, value) 109 | }, 110 | "pass-verbosity-non-standard-map-key-check": func(value interface{}) { 111 | klog.V(verbosityThreshold).InfoS("test", map[string]bool{"key": true}, value) 112 | }, 113 | "fail-verbosity-non-standard-int-key-check": func(value interface{}) { 114 | klog.V(verbosityThreshold+1).InfoS("test", 1, value) 115 | }, 116 | "fail-verbosity-non-standard-struct-key-check": func(value interface{}) { 117 | klog.V(verbosityThreshold+1).InfoS("test", struct{ key string }{"test"}, value) 118 | }, 119 | "fail-verbosity-non-standard-map-key-check": func(value interface{}) { 120 | klog.V(verbosityThreshold+1).InfoS("test", map[string]bool{"key": true}, value) 121 | }, 122 | } 123 | 124 | for name, config := range configs { 125 | b.Run(name, func(b *testing.B) { 126 | if config.cleanup != nil { 127 | defer config.cleanup() 128 | } 129 | config.init() 130 | 131 | for name, logCall := range verbosity { 132 | b.Run(name, func(b *testing.B) { 133 | for _, testcase := range tests { 134 | b.Run(testcase.name, func(b *testing.B) { 135 | b.ResetTimer() 136 | for i := 0; i < b.N; i++ { 137 | logCall(testcase.generate()) 138 | } 139 | }) 140 | } 141 | }) 142 | } 143 | }) 144 | } 145 | } 146 | 147 | func newZaprLogger() logr.Logger { 148 | encoderConfig := &zapcore.EncoderConfig{ 149 | MessageKey: "msg", 150 | CallerKey: "caller", 151 | NameKey: "logger", 152 | EncodeDuration: zapcore.StringDurationEncoder, 153 | EncodeCaller: zapcore.ShortCallerEncoder, 154 | } 155 | encoder := zapcore.NewJSONEncoder(*encoderConfig) 156 | zapV := -zapcore.Level(verbosityThreshold) 157 | core := zapcore.NewCore(encoder, zapcore.AddSync(discard{}), zapV) 158 | l := zap.New(core, zap.WithCaller(true)) 159 | logger := zapr.NewLoggerWithOptions(l, zapr.LogInfoLevel("v"), zapr.ErrorKey("err")) 160 | return logger 161 | } 162 | 163 | type KMetadataMock struct { 164 | Name, NS string 165 | } 166 | 167 | func (m KMetadataMock) GetName() string { 168 | return m.Name 169 | } 170 | func (m KMetadataMock) GetNamespace() string { 171 | return m.NS 172 | } 173 | 174 | type discard struct{} 175 | 176 | var _ io.Writer = discard{} 177 | 178 | func (discard) Write(p []byte) (int, error) { 179 | return len(p), nil 180 | } 181 | -------------------------------------------------------------------------------- /examples/coexist_glog/coexist_glog.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/golang/glog" 7 | "k8s.io/klog/examples/util/require" 8 | "k8s.io/klog/v2" 9 | ) 10 | 11 | func main() { 12 | require.NoError(flag.Set("alsologtostderr", "true")) 13 | flag.Parse() 14 | 15 | klogFlags := flag.NewFlagSet("klog", flag.ExitOnError) 16 | klog.InitFlags(klogFlags) 17 | 18 | // Sync the glog and klog flags. 19 | flag.CommandLine.VisitAll(func(f1 *flag.Flag) { 20 | f2 := klogFlags.Lookup(f1.Name) 21 | if f2 != nil { 22 | value := f1.Value.String() 23 | require.NoError(f2.Value.Set(value)) 24 | } 25 | }) 26 | 27 | glog.Info("hello from glog!") 28 | klog.Info("nice to meet you, I'm klog") 29 | glog.Flush() 30 | klog.Flush() 31 | } 32 | -------------------------------------------------------------------------------- /examples/coexist_klog_v1_and_v2/coexist_klog_v1_and_v2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | klogv1 "k8s.io/klog" 7 | klogv2 "k8s.io/klog/v2" 8 | ) 9 | 10 | // OutputCallDepth is the stack depth where we can find the origin of this call 11 | const OutputCallDepth = 6 12 | 13 | // DefaultPrefixLength is the length of the log prefix that we have to strip out 14 | const DefaultPrefixLength = 53 15 | 16 | // klogWriter is used in SetOutputBySeverity call below to redirect 17 | // any calls to klogv1 to end up in klogv2 18 | type klogWriter struct{} 19 | 20 | func (kw klogWriter) Write(p []byte) (n int, err error) { 21 | if len(p) < DefaultPrefixLength { 22 | klogv2.InfoDepth(OutputCallDepth, string(p)) 23 | return len(p), nil 24 | } 25 | if p[0] == 'I' { 26 | klogv2.InfoDepth(OutputCallDepth, string(p[DefaultPrefixLength:])) 27 | } else if p[0] == 'W' { 28 | klogv2.WarningDepth(OutputCallDepth, string(p[DefaultPrefixLength:])) 29 | } else if p[0] == 'E' { 30 | klogv2.ErrorDepth(OutputCallDepth, string(p[DefaultPrefixLength:])) 31 | } else if p[0] == 'F' { 32 | klogv2.FatalDepth(OutputCallDepth, string(p[DefaultPrefixLength:])) 33 | } else { 34 | klogv2.InfoDepth(OutputCallDepth, string(p[DefaultPrefixLength:])) 35 | } 36 | return len(p), nil 37 | } 38 | 39 | func main() { 40 | // initialize klog/v2, can also bind to a local flagset if desired 41 | klogv2.InitFlags(nil) 42 | 43 | // In this example, we want to show you that all the lines logged 44 | // end up in the myfile.log. You do NOT need them in your application 45 | // as all these flags are set up from the command line typically 46 | flag.Set("logtostderr", "false") // By default klog logs to stderr, switch that off 47 | flag.Set("alsologtostderr", "false") // false is default, but this is informative 48 | flag.Set("stderrthreshold", "FATAL") // stderrthreshold defaults to ERROR, we don't want anything in stderr 49 | flag.Set("log_file", "myfile.log") // log to a file 50 | 51 | // parse klog/v2 flags 52 | flag.Parse() 53 | // make sure we flush before exiting 54 | defer klogv2.Flush() 55 | 56 | // BEGIN : hack to redirect klogv1 calls to klog v2 57 | // Tell klog NOT to log into STDERR. Otherwise, we risk 58 | // certain kinds of API errors getting logged into a directory not 59 | // available in a `FROM scratch` Docker container, causing us to abort 60 | var klogv1Flags flag.FlagSet 61 | klogv1.InitFlags(&klogv1Flags) 62 | klogv1Flags.Set("logtostderr", "false") // By default klog v1 logs to stderr, switch that off 63 | klogv1Flags.Set("stderrthreshold", "FATAL") // stderrthreshold defaults to ERROR, use this if you 64 | // don't want anything in your stderr 65 | 66 | klogv1.SetOutputBySeverity("INFO", klogWriter{}) // tell klog v1 to use the writer 67 | // END : hack to redirect klogv1 calls to klog v2 68 | 69 | // Now you can mix klogv1 and v2 in the same code base 70 | klogv2.Info("hello from klog (v2)!") 71 | klogv1.Info("hello from klog (v1)!") 72 | klogv1.Warning("beware from klog (v1)!") 73 | klogv1.Error("error from klog (v1)!") 74 | klogv2.Info("nice to meet you (v2)") 75 | } 76 | -------------------------------------------------------------------------------- /examples/coexist_klog_v1_and_v2/go.mod: -------------------------------------------------------------------------------- 1 | module k8s.io/klog/examples/coexist_klog_v1_and_v2 2 | 3 | go 1.13 4 | 5 | require ( 6 | k8s.io/klog v1.0.0 7 | k8s.io/klog/v2 v2.0.0 8 | ) 9 | -------------------------------------------------------------------------------- /examples/flushing/.gitignore: -------------------------------------------------------------------------------- 1 | myfile.log 2 | -------------------------------------------------------------------------------- /examples/flushing/flushing_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "testing" 6 | 7 | "go.uber.org/goleak" 8 | 9 | "k8s.io/klog/examples/util/require" 10 | "k8s.io/klog/v2" 11 | ) 12 | 13 | func main() { 14 | klog.InitFlags(nil) 15 | 16 | // By default klog writes to stderr. Setting logtostderr to false makes klog 17 | // write to a log file. 18 | require.NoError(flag.Set("logtostderr", "false")) 19 | require.NoError(flag.Set("log_file", "myfile.log")) 20 | flag.Parse() 21 | 22 | // Info writes the first log message. When the first log file is created, 23 | // a flushDaemon is started to frequently flush bytes to the file. 24 | klog.Info("nice to meet you") 25 | 26 | // klog won't ever stop this flushDaemon. To exit without leaking a goroutine, 27 | // the daemon can be stopped manually. 28 | klog.StopFlushDaemon() 29 | 30 | // After you stopped the flushDaemon, you can still manually flush. 31 | klog.Info("bye") 32 | klog.Flush() 33 | } 34 | 35 | func TestLeakingFlushDaemon(t *testing.T) { 36 | // goleak detects leaking goroutines. 37 | defer goleak.VerifyNone(t) 38 | 39 | // Without calling StopFlushDaemon in main, this test will fail. 40 | main() 41 | } 42 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module k8s.io/klog/examples 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.0 6 | 7 | require ( 8 | github.com/go-logr/logr v1.4.1 9 | github.com/go-logr/zapr v1.2.3 10 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 11 | go.uber.org/goleak v1.1.12 12 | go.uber.org/zap v1.19.0 13 | golang.org/x/tools v0.27.0 14 | k8s.io/klog/v2 v2.30.0 15 | ) 16 | 17 | require ( 18 | go.uber.org/atomic v1.7.0 // indirect 19 | go.uber.org/multierr v1.6.0 // indirect 20 | golang.org/x/mod v0.22.0 // indirect 21 | golang.org/x/sync v0.9.0 // indirect 22 | ) 23 | 24 | replace k8s.io/klog/v2 => ../ 25 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 7 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 8 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 9 | github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= 10 | github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= 11 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 12 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 14 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 16 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 17 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 18 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 19 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 24 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 25 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 26 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 27 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 28 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 29 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 30 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 31 | go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 32 | go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= 33 | go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 34 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 35 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 36 | go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= 37 | go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= 38 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 39 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 40 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 41 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 42 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 43 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 44 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 45 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 46 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 47 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 48 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 49 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 51 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 52 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 53 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 59 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 60 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 61 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 62 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 63 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 64 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 65 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 66 | golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= 67 | golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= 68 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 69 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 70 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 71 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 72 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 73 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 74 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 75 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 76 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 77 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 78 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | -------------------------------------------------------------------------------- /examples/go_vet/go_vet_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | "os/exec" 22 | "path" 23 | "testing" 24 | 25 | "golang.org/x/tools/go/analysis/analysistest" 26 | "golang.org/x/tools/go/analysis/passes/printf" 27 | ) 28 | 29 | // TestGoVet checks that "go vet" detects incorrect klog calls like 30 | // mismatched format specifiers and arguments. 31 | func TestGoVet(t *testing.T) { 32 | testdata := analysistest.TestData() 33 | src := path.Join(testdata, "src") 34 | t.Cleanup(func() { 35 | os.RemoveAll(src) 36 | }) 37 | 38 | // analysistest doesn't support using existing code 39 | // via modules (https://github.com/golang/go/issues/37054). 40 | // Populating the "testdata/src" directory with the 41 | // result of "go mod vendor" is a workaround. 42 | cmd := exec.Command("go", "mod", "vendor", "-o", src) 43 | out, err := cmd.CombinedOutput() 44 | if err != nil { 45 | t.Fatalf("%s failed: %v\nOutput: %s", cmd, err, string(out)) 46 | } 47 | 48 | analyzer := printf.Analyzer 49 | analysistest.Run(t, testdata, analyzer, "") 50 | } 51 | -------------------------------------------------------------------------------- /examples/go_vet/testdata/calls.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package testdata 18 | 19 | import ( 20 | "k8s.io/klog/v2" 21 | ) 22 | 23 | func calls() { 24 | klog.Infof("%s") // want `k8s.io/klog/v2.Infof format %s reads arg #1, but call has 0 args` 25 | klog.Infof("%s", "world") 26 | klog.Info("%s", "world") // want `k8s.io/klog/v2.Info call has possible Printf formatting directive %s` 27 | klog.Info("world") 28 | klog.Infoln("%s", "world") // want `k8s.io/klog/v2.Infoln call has possible Printf formatting directive %s` 29 | klog.Infoln("world") 30 | 31 | klog.InfofDepth(1, "%s") // want `k8s.io/klog/v2.InfofDepth format %s reads arg #1, but call has 0 args` 32 | klog.InfofDepth(1, "%s", "world") 33 | klog.InfoDepth(1, "%s", "world") // want `k8s.io/klog/v2.InfoDepth call has possible Printf formatting directive %s` 34 | klog.InfoDepth(1, "world") 35 | klog.InfolnDepth(1, "%s", "world") // want `k8s.io/klog/v2.InfolnDepth call has possible Printf formatting directive %s` 36 | klog.InfolnDepth(1, "world") 37 | 38 | klog.Warningf("%s") // want `k8s.io/klog/v2.Warningf format %s reads arg #1, but call has 0 args` 39 | klog.Warningf("%s", "world") 40 | klog.Warning("%s", "world") // want `k8s.io/klog/v2.Warning call has possible Printf formatting directive %s` 41 | klog.Warning("world") 42 | klog.Warningln("%s", "world") // want `k8s.io/klog/v2.Warningln call has possible Printf formatting directive %s` 43 | klog.Warningln("world") 44 | 45 | klog.WarningfDepth(1, "%s") // want `k8s.io/klog/v2.WarningfDepth format %s reads arg #1, but call has 0 args` 46 | klog.WarningfDepth(1, "%s", "world") 47 | klog.WarningDepth(1, "%s", "world") // want `k8s.io/klog/v2.WarningDepth call has possible Printf formatting directive %s` 48 | klog.WarningDepth(1, "world") 49 | klog.WarninglnDepth(1, "%s", "world") // want `k8s.io/klog/v2.WarninglnDepth call has possible Printf formatting directive %s` 50 | klog.WarninglnDepth(1, "world") 51 | 52 | klog.Errorf("%s") // want `k8s.io/klog/v2.Errorf format %s reads arg #1, but call has 0 args` 53 | klog.Errorf("%s", "world") 54 | klog.Error("%s", "world") // want `k8s.io/klog/v2.Error call has possible Printf formatting directive %s` 55 | klog.Error("world") 56 | klog.Errorln("%s", "world") // want `k8s.io/klog/v2.Errorln call has possible Printf formatting directive %s` 57 | klog.Errorln("world") 58 | 59 | klog.ErrorfDepth(1, "%s") // want `k8s.io/klog/v2.ErrorfDepth format %s reads arg #1, but call has 0 args` 60 | klog.ErrorfDepth(1, "%s", "world") 61 | klog.ErrorDepth(1, "%s", "world") // want `k8s.io/klog/v2.ErrorDepth call has possible Printf formatting directive %s` 62 | klog.ErrorDepth(1, "world") 63 | klog.ErrorlnDepth(1, "%s", "world") // want `k8s.io/klog/v2.ErrorlnDepth call has possible Printf formatting directive %s` 64 | klog.ErrorlnDepth(1, "world") 65 | 66 | klog.Fatalf("%s") // want `k8s.io/klog/v2.Fatalf format %s reads arg #1, but call has 0 args` 67 | klog.Fatalf("%s", "world") 68 | klog.Fatal("%s", "world") // want `k8s.io/klog/v2.Fatal call has possible Printf formatting directive %s` 69 | klog.Fatal("world") 70 | klog.Fatalln("%s", "world") // want `k8s.io/klog/v2.Fatalln call has possible Printf formatting directive %s` 71 | klog.Fatalln("world") 72 | 73 | klog.FatalfDepth(1, "%s") // want `k8s.io/klog/v2.FatalfDepth format %s reads arg #1, but call has 0 args` 74 | klog.FatalfDepth(1, "%s", "world") 75 | klog.FatalDepth(1, "%s", "world") // want `k8s.io/klog/v2.FatalDepth call has possible Printf formatting directive %s` 76 | klog.FatalDepth(1, "world") 77 | klog.FatallnDepth(1, "%s", "world") // want `k8s.io/klog/v2.FatallnDepth call has possible Printf formatting directive %s` 78 | klog.FatallnDepth(1, "world") 79 | 80 | klog.V(1).Infof("%s") // want `\(k8s.io/klog/v2.Verbose\).Infof format %s reads arg #1, but call has 0 args` 81 | klog.V(1).Infof("%s", "world") 82 | klog.V(1).Info("%s", "world") // want `\(k8s.io/klog/v2.Verbose\).Info call has possible Printf formatting directive %s` 83 | klog.V(1).Info("world") 84 | klog.V(1).Infoln("%s", "world") // want `\(k8s.io/klog/v2.Verbose\).Infoln call has possible Printf formatting directive %s` 85 | klog.V(1).Infoln("world") 86 | 87 | klog.V(1).InfofDepth(1, "%s") // want `\(k8s.io/klog/v2.Verbose\).InfofDepth format %s reads arg #1, but call has 0 args` 88 | klog.V(1).InfofDepth(1, "%s", "world") 89 | klog.V(1).InfoDepth(1, "%s", "world") // want `\(k8s.io/klog/v2.Verbose\).InfoDepth call has possible Printf formatting directive %s` 90 | klog.V(1).InfoDepth(1, "world") 91 | klog.V(1).InfolnDepth(1, "%s", "world") // want `\(k8s.io/klog/v2.Verbose\).InfolnDepth call has possible Printf formatting directive %s` 92 | klog.V(1).InfolnDepth(1, "world") 93 | 94 | // Detecting format specifiers for klog.InfoS and other structured logging calls would be nice, 95 | // but doesn't work the same way because of the extra "msg" string parameter. logcheck 96 | // can be used instead of "go vet". 97 | klog.InfoS("%s", "world") 98 | } 99 | -------------------------------------------------------------------------------- /examples/klogr/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "k8s.io/klog/examples/util/require" 7 | "k8s.io/klog/v2" 8 | "k8s.io/klog/v2/klogr" 9 | ) 10 | 11 | type myError struct { 12 | str string 13 | } 14 | 15 | func (e myError) Error() string { 16 | return e.str 17 | } 18 | 19 | func main() { 20 | klog.InitFlags(nil) 21 | require.NoError(flag.Set("v", "3")) 22 | flag.Parse() 23 | log := klogr.New().WithName("MyName").WithValues("user", "you") 24 | log.Info("hello", "val1", 1, "val2", map[string]int{"k": 1}) 25 | log.V(3).Info("nice to meet you") 26 | log.Error(nil, "uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) 27 | log.Error(myError{"an error occurred"}, "goodbye", "code", -1) 28 | klog.Flush() 29 | } 30 | -------------------------------------------------------------------------------- /examples/log_file/usage_log_file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "k8s.io/klog/examples/util/require" 7 | "k8s.io/klog/v2" 8 | ) 9 | 10 | func main() { 11 | klog.InitFlags(nil) 12 | // By default klog writes to stderr. Setting logtostderr to false makes klog 13 | // write to a log file. 14 | require.NoError(flag.Set("logtostderr", "false")) 15 | require.NoError(flag.Set("log_file", "myfile.log")) 16 | flag.Parse() 17 | klog.Info("nice to meet you") 18 | klog.Flush() 19 | } 20 | -------------------------------------------------------------------------------- /examples/output_test/output_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package output_test shows how to use k8s.io/klog/v2/test 18 | // and provides unit testing with dependencies that wouldn't 19 | // be acceptable for the main module. 20 | package output_test 21 | 22 | import ( 23 | "io" 24 | "testing" 25 | 26 | "github.com/go-logr/logr" 27 | "github.com/go-logr/zapr" 28 | "go.uber.org/zap" 29 | "go.uber.org/zap/zapcore" 30 | 31 | "k8s.io/klog/v2" 32 | "k8s.io/klog/v2/klogr" 33 | "k8s.io/klog/v2/test" 34 | "k8s.io/klog/v2/textlogger" 35 | ) 36 | 37 | // newLogger is a test.OutputConfig.NewLogger callback which creates a zapr 38 | // logger. The vmodule parameter is ignored because zapr does not support that. 39 | func newLogger(out io.Writer, v int, _ string) logr.Logger { 40 | return newZaprLogger(out, v) 41 | } 42 | 43 | // TestZaprOutput tests the zapr, directly and as backend. 44 | func TestZaprOutput(t *testing.T) { 45 | test.InitKlog(t) 46 | t.Run("direct", func(t *testing.T) { 47 | test.Output(t, test.OutputConfig{NewLogger: newLogger, ExpectedOutputMapping: test.ZaprOutputMappingDirect()}) 48 | }) 49 | t.Run("klog-backend", func(t *testing.T) { 50 | test.Output(t, test.OutputConfig{NewLogger: newLogger, AsBackend: true, ExpectedOutputMapping: test.ZaprOutputMappingIndirect()}) 51 | }) 52 | } 53 | 54 | // Benchmark direct zapr output. 55 | func BenchmarkZaprOutput(b *testing.B) { 56 | test.InitKlog(b) 57 | test.Benchmark(b, test.OutputConfig{NewLogger: newLogger, ExpectedOutputMapping: test.ZaprOutputMappingDirect()}) 58 | } 59 | 60 | // TestKlogrStackText tests klogr.klogr -> klog -> text logger. 61 | func TestKlogrStackText(t *testing.T) { 62 | test.InitKlog(t) 63 | newLogger := func(out io.Writer, v int, vmodule string) logr.Logger { 64 | // Backend: text output. 65 | config := textlogger.NewConfig( 66 | textlogger.Verbosity(v), 67 | textlogger.Output(out), 68 | ) 69 | if err := config.VModule().Set(vmodule); err != nil { 70 | panic(err) 71 | } 72 | klog.SetLogger(textlogger.NewLogger(config)) 73 | 74 | // Frontend: klogr. 75 | return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog)) 76 | } 77 | test.Output(t, test.OutputConfig{NewLogger: newLogger, SupportsVModule: true}) 78 | } 79 | 80 | // TestKlogrStackKlogr tests klogr.klogr -> klog -> zapr. 81 | // 82 | // This exposes whether verbosity is passed through correctly 83 | // (https://github.com/kubernetes/klog/issues/294) because klogr logging 84 | // records that. 85 | func TestKlogrStackZapr(t *testing.T) { 86 | test.InitKlog(t) 87 | mapping := test.ZaprOutputMappingIndirect() 88 | 89 | // klogr doesn't warn about invalid KVs and just inserts 90 | // "(MISSING)". 91 | for key, value := range map[string]string{ 92 | `I output.go:] "odd arguments" akey="avalue" akey2="(MISSING)" 93 | `: `{"caller":"test/output.go:","msg":"odd arguments","v":0,"akey":"avalue","akey2":"(MISSING)"} 94 | `, 95 | 96 | `I output.go:] "both odd" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)" 97 | `: `{"caller":"test/output.go:","msg":"both odd","v":0,"basekey1":"basevar1","basekey2":"(MISSING)","akey":"avalue","akey2":"(MISSING)"} 98 | `, 99 | `I output.go:] "integer keys" %!s(int=1)="value" %!s(int=2)="value2" akey="avalue" akey2="(MISSING)" 100 | `: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":1} 101 | {"caller":"test/output.go:","msg":"integer keys","v":0} 102 | `, 103 | `I output.go:] "struct keys" {name}="value" test="other value" key="val" 104 | `: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{}} 105 | {"caller":"test/output.go:","msg":"struct keys","v":0} 106 | `, 107 | `I output.go:] "map keys" map[test:%!s(bool=true)]="test" 108 | `: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{"test":true}} 109 | {"caller":"test/output.go:","msg":"map keys","v":0} 110 | `, 111 | } { 112 | mapping[key] = value 113 | } 114 | 115 | newLogger := func(out io.Writer, v int, _ string) logr.Logger { 116 | // Backend: zapr as configured in k8s.io/component-base/logs/json. 117 | klog.SetLogger(newZaprLogger(out, v)) 118 | 119 | // Frontend: klogr. 120 | return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog)) 121 | } 122 | test.Output(t, test.OutputConfig{NewLogger: newLogger, ExpectedOutputMapping: mapping}) 123 | } 124 | 125 | // TestKlogrInternalStackText tests klog.klogr (the simplified version used for contextual logging) -> klog -> text logger. 126 | func TestKlogrInternalStackText(t *testing.T) { 127 | test.InitKlog(t) 128 | newLogger := func(out io.Writer, v int, vmodule string) logr.Logger { 129 | // Backend: text output. 130 | config := textlogger.NewConfig( 131 | textlogger.Verbosity(v), 132 | textlogger.Output(out), 133 | ) 134 | if err := config.VModule().Set(vmodule); err != nil { 135 | panic(err) 136 | } 137 | klog.SetLogger(textlogger.NewLogger(config)) 138 | 139 | // Frontend: internal klogr. 140 | return klog.NewKlogr() 141 | } 142 | test.Output(t, test.OutputConfig{NewLogger: newLogger, SupportsVModule: true}) 143 | } 144 | 145 | // TestKlogrInternalStackKlogr tests klog.klogr (the simplified version used for contextual logging) -> klog -> zapr. 146 | // 147 | // This exposes whether verbosity is passed through correctly 148 | // (https://github.com/kubernetes/klog/issues/294) because klogr logging 149 | // records that. 150 | func TestKlogrInternalStackZapr(t *testing.T) { 151 | test.InitKlog(t) 152 | mapping := test.ZaprOutputMappingIndirect() 153 | 154 | // klogr doesn't warn about invalid KVs and just inserts 155 | // "(MISSING)". 156 | for key, value := range map[string]string{ 157 | `I output.go:] "odd arguments" akey="avalue" akey2="(MISSING)" 158 | `: `{"caller":"test/output.go:","msg":"odd arguments","v":0,"akey":"avalue","akey2":"(MISSING)"} 159 | `, 160 | 161 | `I output.go:] "both odd" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)" 162 | `: `{"caller":"test/output.go:","msg":"both odd","v":0,"basekey1":"basevar1","basekey2":"(MISSING)","akey":"avalue","akey2":"(MISSING)"} 163 | `, 164 | `I output.go:] "integer keys" %!s(int=1)="value" %!s(int=2)="value2" akey="avalue" akey2="(MISSING)" 165 | `: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":1} 166 | {"caller":"test/output.go:","msg":"integer keys","v":0} 167 | `, 168 | `I output.go:] "struct keys" {name}="value" test="other value" key="val" 169 | `: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{}} 170 | {"caller":"test/output.go:","msg":"struct keys","v":0} 171 | `, 172 | `I output.go:] "map keys" map[test:%!s(bool=true)]="test" 173 | `: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{"test":true}} 174 | {"caller":"test/output.go:","msg":"map keys","v":0} 175 | `, 176 | } { 177 | mapping[key] = value 178 | } 179 | 180 | newLogger := func(out io.Writer, v int, _ string) logr.Logger { 181 | // Backend: zapr as configured in k8s.io/component-base/logs/json. 182 | klog.SetLogger(newZaprLogger(out, v)) 183 | 184 | // Frontend: internal klogr. 185 | return klog.NewKlogr() 186 | } 187 | test.Output(t, test.OutputConfig{NewLogger: newLogger, ExpectedOutputMapping: mapping}) 188 | } 189 | 190 | func newZaprLogger(out io.Writer, v int) logr.Logger { 191 | encoderConfig := &zapcore.EncoderConfig{ 192 | MessageKey: "msg", 193 | CallerKey: "caller", 194 | NameKey: "logger", 195 | EncodeDuration: zapcore.StringDurationEncoder, 196 | EncodeCaller: zapcore.ShortCallerEncoder, 197 | } 198 | encoder := zapcore.NewJSONEncoder(*encoderConfig) 199 | zapV := -zapcore.Level(v) 200 | core := zapcore.NewCore(encoder, zapcore.AddSync(out), zapV) 201 | l := zap.New(core, zap.WithCaller(true)) 202 | logger := zapr.NewLoggerWithOptions(l, zapr.LogInfoLevel("v"), zapr.ErrorKey("err")) 203 | return logger 204 | } 205 | -------------------------------------------------------------------------------- /examples/set_output/usage_set_output.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | 8 | "k8s.io/klog/examples/util/require" 9 | "k8s.io/klog/v2" 10 | ) 11 | 12 | func main() { 13 | klog.InitFlags(nil) 14 | require.NoError(flag.Set("logtostderr", "false")) 15 | require.NoError(flag.Set("alsologtostderr", "false")) 16 | flag.Parse() 17 | 18 | buf := new(bytes.Buffer) 19 | klog.SetOutput(buf) 20 | klog.Info("nice to meet you") 21 | klog.Flush() 22 | 23 | fmt.Printf("LOGGED: %s", buf.String()) 24 | } 25 | -------------------------------------------------------------------------------- /examples/structured_logging/structured_logging.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "k8s.io/klog/v2" 7 | ) 8 | 9 | // MyStruct will be logged via %+v 10 | type MyStruct struct { 11 | Name string 12 | Data string 13 | internal int 14 | } 15 | 16 | // MyStringer will be logged as string, with String providing that string. 17 | type MyString MyStruct 18 | 19 | func (m MyString) String() string { 20 | return m.Name + ": " + m.Data 21 | } 22 | 23 | func main() { 24 | klog.InitFlags(nil) 25 | flag.Parse() 26 | 27 | someData := MyStruct{ 28 | Name: "hello", 29 | Data: "world", 30 | internal: 42, 31 | } 32 | 33 | longData := MyStruct{ 34 | Name: "long", 35 | Data: `Multiple 36 | lines 37 | with quite a bit 38 | of text.`, 39 | } 40 | 41 | logData := MyStruct{ 42 | Name: "log output from some program", 43 | Data: `I0000 12:00:00.000000 123456 main.go:42] Starting 44 | E0000 12:00:01.000000 123456 main.go:43] Failed for some reason 45 | `, 46 | } 47 | 48 | stringData := MyString(longData) 49 | 50 | klog.Infof("someData printed using InfoF: %v", someData) 51 | klog.Infof("longData printed using InfoF: %v", longData) 52 | klog.Infof(`stringData printed using InfoF, 53 | with the message across multiple lines: 54 | %v`, stringData) 55 | klog.Infof("logData printed using InfoF:\n%v", logData) 56 | 57 | klog.Info("=============================================") 58 | 59 | klog.InfoS("using InfoS", "someData", someData) 60 | klog.InfoS("using InfoS", "longData", longData) 61 | klog.InfoS(`using InfoS with 62 | the message across multiple lines`, 63 | "int", 1, 64 | "stringData", stringData, 65 | "str", "another value") 66 | klog.InfoS("using InfoS", "logData", logData) 67 | klog.InfoS("using InfoS", "boolean", true, "int", 1, "float", 0.1) 68 | 69 | // The Kubernetes recommendation is to start the message with uppercase 70 | // and not end with punctuation. See 71 | // https://github.com/kubernetes/community/blob/HEAD/contributors/devel/sig-instrumentation/migration-to-structured-logging.md 72 | klog.InfoS("Did something", "item", "foobar") 73 | // Not recommended, but also works. 74 | klog.InfoS("This is a full sentence.", "item", "foobar") 75 | } 76 | -------------------------------------------------------------------------------- /examples/util/require/require.go: -------------------------------------------------------------------------------- 1 | package require 2 | 3 | func NoError(err error) { 4 | if err != nil { 5 | panic(err) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /exit.go: -------------------------------------------------------------------------------- 1 | // Go support for leveled logs, analogous to https://code.google.com/p/google-glog/ 2 | // 3 | // Copyright 2013 Google Inc. All Rights Reserved. 4 | // Copyright 2022 The Kubernetes Authors. 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | package klog 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | "time" 24 | ) 25 | 26 | var ( 27 | 28 | // ExitFlushTimeout is the timeout that klog has traditionally used during 29 | // calls like Fatal or Exit when flushing log data right before exiting. 30 | // Applications that replace those calls and do not have some specific 31 | // requirements like "exit immediately" can use this value as parameter 32 | // for FlushAndExit. 33 | // 34 | // Can be set for testing purpose or to change the application's 35 | // default. 36 | ExitFlushTimeout = 10 * time.Second 37 | 38 | // OsExit is the function called by FlushAndExit to terminate the program. 39 | // 40 | // Can be set for testing purpose or to change the application's 41 | // default behavior. Note that the function should not simply return 42 | // because callers of functions like Fatal will not expect that. 43 | OsExit = os.Exit 44 | ) 45 | 46 | // FlushAndExit flushes log data for a certain amount of time and then calls 47 | // os.Exit. Combined with some logging call it provides a replacement for 48 | // traditional calls like Fatal or Exit. 49 | func FlushAndExit(flushTimeout time.Duration, exitCode int) { 50 | timeoutFlush(flushTimeout) 51 | OsExit(exitCode) 52 | } 53 | 54 | // timeoutFlush calls Flush and returns when it completes or after timeout 55 | // elapses, whichever happens first. This is needed because the hooks invoked 56 | // by Flush may deadlock when klog.Fatal is called from a hook that holds 57 | // a lock. Flushing also might take too long. 58 | func timeoutFlush(timeout time.Duration) { 59 | done := make(chan bool, 1) 60 | go func() { 61 | Flush() // calls logging.lockAndFlushAll() 62 | done <- true 63 | }() 64 | select { 65 | case <-done: 66 | case <-time.After(timeout): 67 | fmt.Fprintln(os.Stderr, "klog: Flush took longer than", timeout) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /exit_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package klog_test 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "os" 21 | 22 | "k8s.io/klog/v2" 23 | ) 24 | 25 | func ExampleFlushAndExit() { 26 | // Set up klog so that we can test it below. 27 | 28 | var fs flag.FlagSet 29 | klog.InitFlags(&fs) 30 | state := klog.CaptureState() 31 | defer state.Restore() 32 | if err := fs.Set("skip_headers", "true"); err != nil { 33 | panic(err) 34 | } 35 | if err := fs.Set("logtostderr", "false"); err != nil { 36 | panic(err) 37 | } 38 | klog.SetOutput(os.Stdout) 39 | klog.OsExit = func(exitCode int) { 40 | fmt.Printf("os.Exit(%d)\n", exitCode) 41 | } 42 | 43 | // If we were to return or exit without flushing, this message would 44 | // get lost because it is buffered in memory by klog when writing to 45 | // files. Output to stderr is not buffered. 46 | klog.InfoS("exiting...") 47 | exitCode := 10 48 | klog.FlushAndExit(klog.ExitFlushTimeout, exitCode) 49 | 50 | // Output: 51 | // "exiting..." 52 | // os.Exit(10) 53 | } 54 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klog 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "strings" 23 | 24 | "github.com/go-logr/logr" 25 | ) 26 | 27 | // Format wraps a value of an arbitrary type and implement fmt.Stringer and 28 | // logr.Marshaler for them. Stringer returns pretty-printed JSON. MarshalLog 29 | // returns the original value with a type that has no special methods, in 30 | // particular no MarshalLog or MarshalJSON. 31 | // 32 | // Wrapping values like that is useful when the value has a broken 33 | // implementation of these special functions (for example, a type which 34 | // inherits String from TypeMeta, but then doesn't re-implement String) or the 35 | // implementation produces output that is less readable or unstructured (for 36 | // example, the generated String functions for Kubernetes API types). 37 | func Format(obj interface{}) interface{} { 38 | return formatAny{Object: obj} 39 | } 40 | 41 | type formatAny struct { 42 | Object interface{} 43 | } 44 | 45 | func (f formatAny) String() string { 46 | var buffer strings.Builder 47 | encoder := json.NewEncoder(&buffer) 48 | encoder.SetIndent("", " ") 49 | if err := encoder.Encode(&f.Object); err != nil { 50 | return fmt.Sprintf("error marshaling %T to JSON: %v", f, err) 51 | } 52 | return buffer.String() 53 | } 54 | 55 | func (f formatAny) MarshalLog() interface{} { 56 | // Returning a pointer to a pointer ensures that zapr doesn't find a 57 | // fmt.Stringer or logr.Marshaler when it checks the type of the 58 | // value. It then falls back to reflection, which dumps the value being 59 | // pointed to (JSON doesn't have pointers). 60 | ptr := &f.Object 61 | return &ptr 62 | } 63 | 64 | var _ fmt.Stringer = formatAny{} 65 | var _ logr.Marshaler = formatAny{} 66 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klog_test 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "strings" 23 | "testing" 24 | 25 | "k8s.io/klog/v2" 26 | 27 | "github.com/go-logr/logr" 28 | ) 29 | 30 | func TestFormat(t *testing.T) { 31 | obj := config{ 32 | TypeMeta: TypeMeta{ 33 | Kind: "config", 34 | }, 35 | RealField: 42, 36 | } 37 | 38 | assertEqual(t, "kind is config", obj.String(), "config.String()") 39 | assertEqual(t, `{ 40 | "Kind": "config", 41 | "RealField": 42 42 | } 43 | `, klog.Format(obj).(fmt.Stringer).String(), "Format(config).String()") 44 | // fmt.Sprintf would call String if it was available. 45 | str := fmt.Sprintf("%s", klog.Format(obj).(logr.Marshaler).MarshalLog()) 46 | if strings.Contains(str, "kind is config") { 47 | t.Errorf("fmt.Sprintf called TypeMeta.String for klog.Format(obj).MarshalLog():\n%s", str) 48 | } 49 | 50 | structured, err := json.Marshal(klog.Format(obj).(logr.Marshaler).MarshalLog()) 51 | if err != nil { 52 | t.Errorf("JSON Marshal: %v", err) 53 | } else { 54 | assertEqual(t, `{"Kind":"config","RealField":42}`, string(structured), "json.Marshal(klog.Format(obj).MarshalLog())") 55 | } 56 | } 57 | 58 | func assertEqual(t *testing.T, expected, actual, what string) { 59 | if expected != actual { 60 | t.Errorf("%s:\nExpected\n%s\nActual\n%s\n", what, expected, actual) 61 | } 62 | } 63 | 64 | type TypeMeta struct { 65 | Kind string 66 | } 67 | 68 | func (t TypeMeta) String() string { 69 | return "kind is " + t.Kind 70 | } 71 | 72 | func (t TypeMeta) MarshalLog() interface{} { 73 | return t.Kind 74 | } 75 | 76 | type config struct { 77 | TypeMeta 78 | 79 | RealField int 80 | } 81 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module k8s.io/klog/v2 2 | 3 | go 1.21 4 | 5 | require github.com/go-logr/logr v1.4.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 2 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 3 | -------------------------------------------------------------------------------- /hack/verify-apidiff.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | function usage { 22 | local script="$(basename $0)" 23 | 24 | echo >&2 "Usage: ${script} [-r | -d ] 25 | 26 | This script should be run at the root of a module. 27 | 28 | -r 29 | Compare the exported API of the local working copy with the 30 | exported API of the local repo at the specified branch or tag. 31 | 32 | -d 33 | Compare the exported API of the local working copy with the 34 | exported API of the specified directory, which should point 35 | to the root of a different version of the same module. 36 | 37 | Examples: 38 | ${script} -r master 39 | ${script} -r v1.10.0 40 | ${script} -r release-1.10 41 | ${script} -d /path/to/historical/version 42 | " 43 | exit 1 44 | } 45 | 46 | ref="" 47 | dir="" 48 | while getopts r:d: o 49 | do case "$o" in 50 | r) ref="$OPTARG";; 51 | d) dir="$OPTARG";; 52 | [?]) usage;; 53 | esac 54 | done 55 | 56 | # If REF and DIR are empty, print usage and error 57 | if [[ -z "${ref}" && -z "${dir}" ]]; then 58 | usage; 59 | fi 60 | # If REF and DIR are both set, print usage and error 61 | if [[ -n "${ref}" && -n "${dir}" ]]; then 62 | usage; 63 | fi 64 | 65 | if ! which apidiff > /dev/null; then 66 | echo "Installing golang.org/x/exp/cmd/apidiff..." 67 | pushd "${TMPDIR:-/tmp}" > /dev/null 68 | GO111MODULE=off go get golang.org/x/exp/cmd/apidiff 69 | popd > /dev/null 70 | fi 71 | 72 | output=$(mktemp -d -t "apidiff.output.XXXX") 73 | cleanup_output () { rm -fr "${output}"; } 74 | trap cleanup_output EXIT 75 | 76 | # If ref is set, clone . to temp dir at $ref, and set $dir to the temp dir 77 | clone="" 78 | base="${dir}" 79 | if [[ -n "${ref}" ]]; then 80 | base="${ref}" 81 | clone=$(mktemp -d -t "apidiff.clone.XXXX") 82 | cleanup_clone_and_output () { rm -fr "${clone}"; cleanup_output; } 83 | trap cleanup_clone_and_output EXIT 84 | git clone . -q --no-tags -b "${ref}" "${clone}" 85 | dir="${clone}" 86 | fi 87 | 88 | pushd "${dir}" >/dev/null 89 | echo "Inspecting API of ${base}..." 90 | go list ./... > packages.txt 91 | for pkg in $(cat packages.txt); do 92 | mkdir -p "${output}/${pkg}" 93 | apidiff -w "${output}/${pkg}/apidiff.output" "${pkg}" 94 | done 95 | popd >/dev/null 96 | 97 | retval=0 98 | 99 | echo "Comparing with ${base}..." 100 | for pkg in $(go list ./...); do 101 | # New packages are ok 102 | if [ ! -f "${output}/${pkg}/apidiff.output" ]; then 103 | continue 104 | fi 105 | 106 | # Check for incompatible changes to previous packages 107 | incompatible=$(apidiff -incompatible "${output}/${pkg}/apidiff.output" "${pkg}") 108 | if [[ -n "${incompatible}" ]]; then 109 | echo >&2 "FAIL: ${pkg} contains incompatible changes: 110 | ${incompatible} 111 | " 112 | retval=1 113 | fi 114 | done 115 | 116 | # Check for removed packages 117 | removed=$(comm -23 "${dir}/packages.txt" <(go list ./...)) 118 | if [[ -n "${removed}" ]]; then 119 | echo >&2 "FAIL: removed packages: 120 | ${removed} 121 | " 122 | retval=1 123 | fi 124 | 125 | exit $retval 126 | -------------------------------------------------------------------------------- /imports.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klog 18 | 19 | import ( 20 | "github.com/go-logr/logr" 21 | ) 22 | 23 | // The reason for providing these aliases is to allow code to work with logr 24 | // without directly importing it. 25 | 26 | // Logger in this package is exactly the same as logr.Logger. 27 | type Logger = logr.Logger 28 | 29 | // LogSink in this package is exactly the same as logr.LogSink. 30 | type LogSink = logr.LogSink 31 | 32 | // Runtimeinfo in this package is exactly the same as logr.RuntimeInfo. 33 | type RuntimeInfo = logr.RuntimeInfo 34 | 35 | var ( 36 | // New is an alias for logr.New. 37 | New = logr.New 38 | ) 39 | -------------------------------------------------------------------------------- /integration_tests/internal/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This file is intended to be used as a standin for a klog'ed executable. 4 | 5 | It is called by the integration test via `go run` and with different klog 6 | flags to assert on klog behaviour, especially where klog logs its output 7 | when different combinations of the klog flags are at play. 8 | 9 | This file is not intended to be used outside of the integration tests and 10 | is not supposed to be a (good) example on how to use klog. 11 | 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "flag" 18 | "fmt" 19 | "os" 20 | 21 | "k8s.io/klog/v2" 22 | ) 23 | 24 | func main() { 25 | infoLogLine := getEnvOrDie("KLOG_INFO_LOG") 26 | warningLogLine := getEnvOrDie("KLOG_WARNING_LOG") 27 | errorLogLine := getEnvOrDie("KLOG_ERROR_LOG") 28 | fatalLogLine := getEnvOrDie("KLOG_FATAL_LOG") 29 | 30 | klog.InitFlags(nil) 31 | flag.Parse() 32 | klog.Info(infoLogLine) 33 | klog.Warning(warningLogLine) 34 | klog.Error(errorLogLine) 35 | klog.Flush() 36 | klog.Fatal(fatalLogLine) 37 | } 38 | 39 | func getEnvOrDie(name string) string { 40 | val, ok := os.LookupEnv(name) 41 | if !ok { 42 | fmt.Fprintln(os.Stderr, name, "could not be found in environment") 43 | os.Exit(1) 44 | } 45 | return val 46 | } 47 | -------------------------------------------------------------------------------- /internal/buffer/buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All Rights Reserved. 2 | // Copyright 2022 The Kubernetes Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | // Package buffer provides a cache for byte.Buffer instances that can be reused 17 | // to avoid frequent allocation and deallocation. It also has utility code 18 | // for log header formatting that use these buffers. 19 | package buffer 20 | 21 | import ( 22 | "bytes" 23 | "os" 24 | "sync" 25 | "time" 26 | 27 | "k8s.io/klog/v2/internal/severity" 28 | ) 29 | 30 | var ( 31 | // Pid is inserted into log headers. Can be overridden for tests. 32 | Pid = os.Getpid() 33 | 34 | // Time, if set, will be used instead of the actual current time. 35 | Time *time.Time 36 | ) 37 | 38 | // Buffer holds a single byte.Buffer for reuse. The zero value is ready for 39 | // use. It also provides some helper methods for output formatting. 40 | type Buffer struct { 41 | bytes.Buffer 42 | Tmp [64]byte // temporary byte array for creating headers. 43 | } 44 | 45 | var buffers = sync.Pool{ 46 | New: func() interface{} { 47 | return new(Buffer) 48 | }, 49 | } 50 | 51 | // GetBuffer returns a new, ready-to-use buffer. 52 | func GetBuffer() *Buffer { 53 | b := buffers.Get().(*Buffer) 54 | b.Reset() 55 | return b 56 | } 57 | 58 | // PutBuffer returns a buffer to the free list. 59 | func PutBuffer(b *Buffer) { 60 | if b.Len() >= 256 { 61 | // Let big buffers die a natural death, without relying on 62 | // sync.Pool behavior. The documentation implies that items may 63 | // get deallocated while stored there ("If the Pool holds the 64 | // only reference when this [= be removed automatically] 65 | // happens, the item might be deallocated."), but 66 | // https://github.com/golang/go/issues/23199 leans more towards 67 | // having such a size limit. 68 | return 69 | } 70 | 71 | buffers.Put(b) 72 | } 73 | 74 | // Some custom tiny helper functions to print the log header efficiently. 75 | 76 | const digits = "0123456789" 77 | 78 | // twoDigits formats a zero-prefixed two-digit integer at buf.Tmp[i]. 79 | func (buf *Buffer) twoDigits(i, d int) { 80 | buf.Tmp[i+1] = digits[d%10] 81 | d /= 10 82 | buf.Tmp[i] = digits[d%10] 83 | } 84 | 85 | // nDigits formats an n-digit integer at buf.Tmp[i], 86 | // padding with pad on the left. 87 | // It assumes d >= 0. 88 | func (buf *Buffer) nDigits(n, i, d int, pad byte) { 89 | j := n - 1 90 | for ; j >= 0 && d > 0; j-- { 91 | buf.Tmp[i+j] = digits[d%10] 92 | d /= 10 93 | } 94 | for ; j >= 0; j-- { 95 | buf.Tmp[i+j] = pad 96 | } 97 | } 98 | 99 | // someDigits formats a zero-prefixed variable-width integer at buf.Tmp[i]. 100 | func (buf *Buffer) someDigits(i, d int) int { 101 | // Print into the top, then copy down. We know there's space for at least 102 | // a 10-digit number. 103 | j := len(buf.Tmp) 104 | for { 105 | j-- 106 | buf.Tmp[j] = digits[d%10] 107 | d /= 10 108 | if d == 0 { 109 | break 110 | } 111 | } 112 | return copy(buf.Tmp[i:], buf.Tmp[j:]) 113 | } 114 | 115 | // FormatHeader formats a log header using the provided file name and line number 116 | // and writes it into the buffer. 117 | func (buf *Buffer) FormatHeader(s severity.Severity, file string, line int, now time.Time) { 118 | if line < 0 { 119 | line = 0 // not a real line number, but acceptable to someDigits 120 | } 121 | if s > severity.FatalLog { 122 | s = severity.InfoLog // for safety. 123 | } 124 | 125 | // Avoid Fprintf, for speed. The format is so simple that we can do it quickly by hand. 126 | // It's worth about 3X. Fprintf is hard. 127 | if Time != nil { 128 | now = *Time 129 | } 130 | _, month, day := now.Date() 131 | hour, minute, second := now.Clock() 132 | // Lmmdd hh:mm:ss.uuuuuu threadid file:line] 133 | buf.Tmp[0] = severity.Char[s] 134 | buf.twoDigits(1, int(month)) 135 | buf.twoDigits(3, day) 136 | buf.Tmp[5] = ' ' 137 | buf.twoDigits(6, hour) 138 | buf.Tmp[8] = ':' 139 | buf.twoDigits(9, minute) 140 | buf.Tmp[11] = ':' 141 | buf.twoDigits(12, second) 142 | buf.Tmp[14] = '.' 143 | buf.nDigits(6, 15, now.Nanosecond()/1000, '0') 144 | buf.Tmp[21] = ' ' 145 | buf.nDigits(7, 22, Pid, ' ') // TODO: should be TID 146 | buf.Tmp[29] = ' ' 147 | buf.Write(buf.Tmp[:30]) 148 | buf.WriteString(file) 149 | buf.Tmp[0] = ':' 150 | n := buf.someDigits(1, line) 151 | buf.Tmp[n+1] = ']' 152 | buf.Tmp[n+2] = ' ' 153 | buf.Write(buf.Tmp[:n+3]) 154 | } 155 | 156 | // SprintHeader formats a log header and returns a string. This is a simpler 157 | // version of FormatHeader for use in ktesting. 158 | func (buf *Buffer) SprintHeader(s severity.Severity, now time.Time) string { 159 | if s > severity.FatalLog { 160 | s = severity.InfoLog // for safety. 161 | } 162 | 163 | // Avoid Fprintf, for speed. The format is so simple that we can do it quickly by hand. 164 | // It's worth about 3X. Fprintf is hard. 165 | if Time != nil { 166 | now = *Time 167 | } 168 | _, month, day := now.Date() 169 | hour, minute, second := now.Clock() 170 | // Lmmdd hh:mm:ss.uuuuuu threadid file:line] 171 | buf.Tmp[0] = severity.Char[s] 172 | buf.twoDigits(1, int(month)) 173 | buf.twoDigits(3, day) 174 | buf.Tmp[5] = ' ' 175 | buf.twoDigits(6, hour) 176 | buf.Tmp[8] = ':' 177 | buf.twoDigits(9, minute) 178 | buf.Tmp[11] = ':' 179 | buf.twoDigits(12, second) 180 | buf.Tmp[14] = '.' 181 | buf.nDigits(6, 15, now.Nanosecond()/1000, '0') 182 | buf.Tmp[21] = ']' 183 | return string(buf.Tmp[:22]) 184 | } 185 | -------------------------------------------------------------------------------- /internal/clock/README.md: -------------------------------------------------------------------------------- 1 | # Clock 2 | 3 | This package provides an interface for time-based operations. It allows 4 | mocking time for testing. 5 | 6 | This is a copy of k8s.io/utils/clock. We have to copy it to avoid a circular 7 | dependency (k8s.io/klog -> k8s.io/utils -> k8s.io/klog). 8 | -------------------------------------------------------------------------------- /internal/clock/clock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package clock 18 | 19 | import "time" 20 | 21 | // PassiveClock allows for injecting fake or real clocks into code 22 | // that needs to read the current time but does not support scheduling 23 | // activity in the future. 24 | type PassiveClock interface { 25 | Now() time.Time 26 | Since(time.Time) time.Duration 27 | } 28 | 29 | // Clock allows for injecting fake or real clocks into code that 30 | // needs to do arbitrary things based on time. 31 | type Clock interface { 32 | PassiveClock 33 | // After returns the channel of a new Timer. 34 | // This method does not allow to free/GC the backing timer before it fires. Use 35 | // NewTimer instead. 36 | After(d time.Duration) <-chan time.Time 37 | // NewTimer returns a new Timer. 38 | NewTimer(d time.Duration) Timer 39 | // Sleep sleeps for the provided duration d. 40 | // Consider making the sleep interruptible by using 'select' on a context channel and a timer channel. 41 | Sleep(d time.Duration) 42 | // NewTicker returns a new Ticker. 43 | NewTicker(time.Duration) Ticker 44 | } 45 | 46 | // WithDelayedExecution allows for injecting fake or real clocks into 47 | // code that needs to make use of AfterFunc functionality. 48 | type WithDelayedExecution interface { 49 | Clock 50 | // AfterFunc executes f in its own goroutine after waiting 51 | // for d duration and returns a Timer whose channel can be 52 | // closed by calling Stop() on the Timer. 53 | AfterFunc(d time.Duration, f func()) Timer 54 | } 55 | 56 | // WithTickerAndDelayedExecution allows for injecting fake or real clocks 57 | // into code that needs Ticker and AfterFunc functionality 58 | type WithTickerAndDelayedExecution interface { 59 | Clock 60 | // AfterFunc executes f in its own goroutine after waiting 61 | // for d duration and returns a Timer whose channel can be 62 | // closed by calling Stop() on the Timer. 63 | AfterFunc(d time.Duration, f func()) Timer 64 | } 65 | 66 | // Ticker defines the Ticker interface. 67 | type Ticker interface { 68 | C() <-chan time.Time 69 | Stop() 70 | } 71 | 72 | var _ Clock = RealClock{} 73 | 74 | // RealClock really calls time.Now() 75 | type RealClock struct{} 76 | 77 | // Now returns the current time. 78 | func (RealClock) Now() time.Time { 79 | return time.Now() 80 | } 81 | 82 | // Since returns time since the specified timestamp. 83 | func (RealClock) Since(ts time.Time) time.Duration { 84 | return time.Since(ts) 85 | } 86 | 87 | // After is the same as time.After(d). 88 | // This method does not allow to free/GC the backing timer before it fires. Use 89 | // NewTimer instead. 90 | func (RealClock) After(d time.Duration) <-chan time.Time { 91 | return time.After(d) 92 | } 93 | 94 | // NewTimer is the same as time.NewTimer(d) 95 | func (RealClock) NewTimer(d time.Duration) Timer { 96 | return &realTimer{ 97 | timer: time.NewTimer(d), 98 | } 99 | } 100 | 101 | // AfterFunc is the same as time.AfterFunc(d, f). 102 | func (RealClock) AfterFunc(d time.Duration, f func()) Timer { 103 | return &realTimer{ 104 | timer: time.AfterFunc(d, f), 105 | } 106 | } 107 | 108 | // NewTicker returns a new Ticker. 109 | func (RealClock) NewTicker(d time.Duration) Ticker { 110 | return &realTicker{ 111 | ticker: time.NewTicker(d), 112 | } 113 | } 114 | 115 | // Sleep is the same as time.Sleep(d) 116 | // Consider making the sleep interruptible by using 'select' on a context channel and a timer channel. 117 | func (RealClock) Sleep(d time.Duration) { 118 | time.Sleep(d) 119 | } 120 | 121 | // Timer allows for injecting fake or real timers into code that 122 | // needs to do arbitrary things based on time. 123 | type Timer interface { 124 | C() <-chan time.Time 125 | Stop() bool 126 | Reset(d time.Duration) bool 127 | } 128 | 129 | var _ = Timer(&realTimer{}) 130 | 131 | // realTimer is backed by an actual time.Timer. 132 | type realTimer struct { 133 | timer *time.Timer 134 | } 135 | 136 | // C returns the underlying timer's channel. 137 | func (r *realTimer) C() <-chan time.Time { 138 | return r.timer.C 139 | } 140 | 141 | // Stop calls Stop() on the underlying timer. 142 | func (r *realTimer) Stop() bool { 143 | return r.timer.Stop() 144 | } 145 | 146 | // Reset calls Reset() on the underlying timer. 147 | func (r *realTimer) Reset(d time.Duration) bool { 148 | return r.timer.Reset(d) 149 | } 150 | 151 | type realTicker struct { 152 | ticker *time.Ticker 153 | } 154 | 155 | func (r *realTicker) C() <-chan time.Time { 156 | return r.ticker.C 157 | } 158 | 159 | func (r *realTicker) Stop() { 160 | r.ticker.Stop() 161 | } 162 | -------------------------------------------------------------------------------- /internal/clock/testing/simple_interval_clock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package testing 18 | 19 | import ( 20 | "time" 21 | 22 | "k8s.io/klog/v2/internal/clock" 23 | ) 24 | 25 | var ( 26 | _ = clock.PassiveClock(&SimpleIntervalClock{}) 27 | ) 28 | 29 | // SimpleIntervalClock implements clock.PassiveClock, but each invocation of Now steps the clock forward the specified duration 30 | type SimpleIntervalClock struct { 31 | Time time.Time 32 | Duration time.Duration 33 | } 34 | 35 | // Now returns i's time. 36 | func (i *SimpleIntervalClock) Now() time.Time { 37 | i.Time = i.Time.Add(i.Duration) 38 | return i.Time 39 | } 40 | 41 | // Since returns time since the time in i. 42 | func (i *SimpleIntervalClock) Since(ts time.Time) time.Duration { 43 | return i.Time.Sub(ts) 44 | } 45 | -------------------------------------------------------------------------------- /internal/dbg/dbg.go: -------------------------------------------------------------------------------- 1 | // Go support for leveled logs, analogous to https://code.google.com/p/google-glog/ 2 | // 3 | // Copyright 2013 Google Inc. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | // Package dbg provides some helper code for call traces. 18 | package dbg 19 | 20 | import ( 21 | "runtime" 22 | ) 23 | 24 | // Stacks is a wrapper for runtime.Stack that attempts to recover the data for 25 | // all goroutines or the calling one. 26 | func Stacks(all bool) []byte { 27 | // We don't know how big the traces are, so grow a few times if they don't fit. Start large, though. 28 | n := 10000 29 | if all { 30 | n = 100000 31 | } 32 | var trace []byte 33 | for i := 0; i < 5; i++ { 34 | trace = make([]byte, n) 35 | nbytes := runtime.Stack(trace, all) 36 | if nbytes < len(trace) { 37 | return trace[:nbytes] 38 | } 39 | n *= 2 40 | } 41 | return trace 42 | } 43 | -------------------------------------------------------------------------------- /internal/serialize/keyvalues_no_slog.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.21 2 | // +build !go1.21 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package serialize 21 | 22 | import ( 23 | "bytes" 24 | "fmt" 25 | 26 | "github.com/go-logr/logr" 27 | ) 28 | 29 | // KVFormat serializes one key/value pair into the provided buffer. 30 | // A space gets inserted before the pair. 31 | func (f Formatter) KVFormat(b *bytes.Buffer, k, v interface{}) string { 32 | // This is the version without slog support. Must be kept in sync with 33 | // the version in keyvalues_slog.go. 34 | 35 | b.WriteByte(' ') 36 | // Keys are assumed to be well-formed according to 37 | // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments 38 | // for the sake of performance. Keys with spaces, 39 | // special characters, etc. will break parsing. 40 | var key string 41 | if sK, ok := k.(string); ok { 42 | // Avoid one allocation when the key is a string, which 43 | // normally it should be. 44 | key = sK 45 | } else { 46 | key = fmt.Sprintf("%s", k) 47 | } 48 | b.WriteString(key) 49 | 50 | // The type checks are sorted so that more frequently used ones 51 | // come first because that is then faster in the common 52 | // cases. In Kubernetes, ObjectRef (a Stringer) is more common 53 | // than plain strings 54 | // (https://github.com/kubernetes/kubernetes/pull/106594#issuecomment-975526235). 55 | switch v := v.(type) { 56 | case textWriter: 57 | writeTextWriterValue(b, v) 58 | case fmt.Stringer: 59 | writeStringValue(b, StringerToString(v)) 60 | case string: 61 | writeStringValue(b, v) 62 | case error: 63 | writeStringValue(b, ErrorToString(v)) 64 | case logr.Marshaler: 65 | value := MarshalerToValue(v) 66 | // A marshaler that returns a string is useful for 67 | // delayed formatting of complex values. We treat this 68 | // case like a normal string. This is useful for 69 | // multi-line support. 70 | // 71 | // We could do this by recursively formatting a value, 72 | // but that comes with the risk of infinite recursion 73 | // if a marshaler returns itself. Instead we call it 74 | // only once and rely on it returning the intended 75 | // value directly. 76 | switch value := value.(type) { 77 | case string: 78 | writeStringValue(b, value) 79 | default: 80 | f.formatAny(b, value) 81 | } 82 | case []byte: 83 | // In https://github.com/kubernetes/klog/pull/237 it was decided 84 | // to format byte slices with "%+q". The advantages of that are: 85 | // - readable output if the bytes happen to be printable 86 | // - non-printable bytes get represented as unicode escape 87 | // sequences (\uxxxx) 88 | // 89 | // The downsides are that we cannot use the faster 90 | // strconv.Quote here and that multi-line output is not 91 | // supported. If developers know that a byte array is 92 | // printable and they want multi-line output, they can 93 | // convert the value to string before logging it. 94 | b.WriteByte('=') 95 | b.WriteString(fmt.Sprintf("%+q", v)) 96 | default: 97 | f.formatAny(b, v) 98 | } 99 | 100 | return key 101 | } 102 | -------------------------------------------------------------------------------- /internal/serialize/keyvalues_slog.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package serialize 21 | 22 | import ( 23 | "bytes" 24 | "fmt" 25 | "log/slog" 26 | "strconv" 27 | 28 | "github.com/go-logr/logr" 29 | ) 30 | 31 | // KVFormat serializes one key/value pair into the provided buffer. 32 | // A space gets inserted before the pair. It returns the key. 33 | func (f Formatter) KVFormat(b *bytes.Buffer, k, v interface{}) string { 34 | // This is the version without slog support. Must be kept in sync with 35 | // the version in keyvalues_slog.go. 36 | 37 | b.WriteByte(' ') 38 | // Keys are assumed to be well-formed according to 39 | // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments 40 | // for the sake of performance. Keys with spaces, 41 | // special characters, etc. will break parsing. 42 | var key string 43 | if sK, ok := k.(string); ok { 44 | // Avoid one allocation when the key is a string, which 45 | // normally it should be. 46 | key = sK 47 | } else { 48 | key = fmt.Sprintf("%s", k) 49 | } 50 | b.WriteString(key) 51 | 52 | // The type checks are sorted so that more frequently used ones 53 | // come first because that is then faster in the common 54 | // cases. In Kubernetes, ObjectRef (a Stringer) is more common 55 | // than plain strings 56 | // (https://github.com/kubernetes/kubernetes/pull/106594#issuecomment-975526235). 57 | // 58 | // slog.LogValuer does not need to be handled here because the handler will 59 | // already have resolved such special values to the final value for logging. 60 | switch v := v.(type) { 61 | case textWriter: 62 | writeTextWriterValue(b, v) 63 | case slog.Value: 64 | // This must come before fmt.Stringer because slog.Value implements 65 | // fmt.Stringer, but does not produce the output that we want. 66 | b.WriteByte('=') 67 | generateJSON(b, v) 68 | case fmt.Stringer: 69 | writeStringValue(b, StringerToString(v)) 70 | case string: 71 | writeStringValue(b, v) 72 | case error: 73 | writeStringValue(b, ErrorToString(v)) 74 | case logr.Marshaler: 75 | value := MarshalerToValue(v) 76 | // A marshaler that returns a string is useful for 77 | // delayed formatting of complex values. We treat this 78 | // case like a normal string. This is useful for 79 | // multi-line support. 80 | // 81 | // We could do this by recursively formatting a value, 82 | // but that comes with the risk of infinite recursion 83 | // if a marshaler returns itself. Instead we call it 84 | // only once and rely on it returning the intended 85 | // value directly. 86 | switch value := value.(type) { 87 | case string: 88 | writeStringValue(b, value) 89 | default: 90 | f.formatAny(b, value) 91 | } 92 | case slog.LogValuer: 93 | value := slog.AnyValue(v).Resolve() 94 | if value.Kind() == slog.KindString { 95 | writeStringValue(b, value.String()) 96 | } else { 97 | b.WriteByte('=') 98 | generateJSON(b, value) 99 | } 100 | case []byte: 101 | // In https://github.com/kubernetes/klog/pull/237 it was decided 102 | // to format byte slices with "%+q". The advantages of that are: 103 | // - readable output if the bytes happen to be printable 104 | // - non-printable bytes get represented as unicode escape 105 | // sequences (\uxxxx) 106 | // 107 | // The downsides are that we cannot use the faster 108 | // strconv.Quote here and that multi-line output is not 109 | // supported. If developers know that a byte array is 110 | // printable and they want multi-line output, they can 111 | // convert the value to string before logging it. 112 | b.WriteByte('=') 113 | b.WriteString(fmt.Sprintf("%+q", v)) 114 | default: 115 | f.formatAny(b, v) 116 | } 117 | 118 | return key 119 | } 120 | 121 | // generateJSON has the same preference for plain strings as KVFormat. 122 | // In contrast to KVFormat it always produces valid JSON with no line breaks. 123 | func generateJSON(b *bytes.Buffer, v interface{}) { 124 | switch v := v.(type) { 125 | case slog.Value: 126 | switch v.Kind() { 127 | case slog.KindGroup: 128 | // Format as a JSON group. We must not involve f.AnyToStringHook (if there is any), 129 | // because there is no guarantee that it produces valid JSON. 130 | b.WriteByte('{') 131 | for i, attr := range v.Group() { 132 | if i > 0 { 133 | b.WriteByte(',') 134 | } 135 | b.WriteString(strconv.Quote(attr.Key)) 136 | b.WriteByte(':') 137 | generateJSON(b, attr.Value) 138 | } 139 | b.WriteByte('}') 140 | case slog.KindLogValuer: 141 | generateJSON(b, v.Resolve()) 142 | default: 143 | // Peel off the slog.Value wrapper and format the actual value. 144 | generateJSON(b, v.Any()) 145 | } 146 | case fmt.Stringer: 147 | b.WriteString(strconv.Quote(StringerToString(v))) 148 | case logr.Marshaler: 149 | generateJSON(b, MarshalerToValue(v)) 150 | case slog.LogValuer: 151 | generateJSON(b, slog.AnyValue(v).Resolve().Any()) 152 | case string: 153 | b.WriteString(strconv.Quote(v)) 154 | case error: 155 | b.WriteString(strconv.Quote(v.Error())) 156 | default: 157 | formatAsJSON(b, v) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /internal/serialize/keyvalues_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package serialize_test 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "testing" 23 | "time" 24 | 25 | "k8s.io/klog/v2" 26 | "k8s.io/klog/v2/internal/serialize" 27 | "k8s.io/klog/v2/internal/test" 28 | ) 29 | 30 | // point conforms to fmt.Stringer interface as it implements the String() method 31 | type point struct { 32 | x int 33 | y int 34 | } 35 | 36 | // we now have a value receiver 37 | func (p point) String() string { 38 | return fmt.Sprintf("x=%d, y=%d", p.x, p.y) 39 | } 40 | 41 | type dummyStruct struct { 42 | key string 43 | value string 44 | } 45 | 46 | func (d *dummyStruct) MarshalLog() interface{} { 47 | return map[string]string{ 48 | "key-data": d.key, 49 | "value-data": d.value, 50 | } 51 | } 52 | 53 | type dummyStructWithStringMarshal struct { 54 | key string 55 | value string 56 | } 57 | 58 | func (d *dummyStructWithStringMarshal) MarshalLog() interface{} { 59 | return fmt.Sprintf("%s=%s", d.key, d.value) 60 | } 61 | 62 | // Test that kvListFormat works as advertised. 63 | func TestKvListFormat(t *testing.T) { 64 | var emptyPoint *point 65 | var testKVList = []struct { 66 | keysValues []interface{} 67 | want string 68 | }{ 69 | { 70 | keysValues: []interface{}{"data", &dummyStruct{key: "test", value: "info"}}, 71 | want: ` data={"key-data":"test","value-data":"info"}`, 72 | }, 73 | { 74 | keysValues: []interface{}{"data", &dummyStructWithStringMarshal{key: "test", value: "info"}}, 75 | want: ` data="test=info"`, 76 | }, 77 | { 78 | keysValues: []interface{}{"pod", "kubedns"}, 79 | want: " pod=\"kubedns\"", 80 | }, 81 | { 82 | keysValues: []interface{}{"pod", "kubedns", "update", true}, 83 | want: " pod=\"kubedns\" update=true", 84 | }, 85 | { 86 | keysValues: []interface{}{"pod", "kubedns", "spec", struct { 87 | X int 88 | Y string 89 | N time.Time 90 | }{X: 76, Y: "strval", N: time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.UTC)}}, 91 | want: ` pod="kubedns" spec={"X":76,"Y":"strval","N":"2006-01-02T15:04:05.06789Z"}`, 92 | }, 93 | { 94 | keysValues: []interface{}{"pod", "kubedns", "values", []int{8, 6, 7, 5, 3, 0, 9}}, 95 | want: " pod=\"kubedns\" values=[8,6,7,5,3,0,9]", 96 | }, 97 | { 98 | keysValues: []interface{}{"pod", "kubedns", "values", []string{"deployment", "svc", "configmap"}}, 99 | want: ` pod="kubedns" values=["deployment","svc","configmap"]`, 100 | }, 101 | { 102 | keysValues: []interface{}{"pod", "kubedns", "bytes", []byte("test case for byte array")}, 103 | want: " pod=\"kubedns\" bytes=\"test case for byte array\"", 104 | }, 105 | { 106 | keysValues: []interface{}{"pod", "kubedns", "bytes", []byte("��=� ⌘")}, 107 | want: " pod=\"kubedns\" bytes=\"\\ufffd\\ufffd=\\ufffd \\u2318\"", 108 | }, 109 | { 110 | keysValues: []interface{}{"multiLineString", `Hello world! 111 | Starts with tab. 112 | Starts with spaces. 113 | No whitespace.`, 114 | "pod", "kubedns", 115 | }, 116 | want: ` multiLineString=< 117 | Hello world! 118 | Starts with tab. 119 | Starts with spaces. 120 | No whitespace. 121 | > pod="kubedns"`, 122 | }, 123 | { 124 | keysValues: []interface{}{"pod", "kubedns", "maps", map[string]int{"three": 4}}, 125 | want: ` pod="kubedns" maps={"three":4}`, 126 | }, 127 | { 128 | keysValues: []interface{}{"pod", klog.KRef("kube-system", "kubedns"), "status", "ready"}, 129 | want: " pod=\"kube-system/kubedns\" status=\"ready\"", 130 | }, 131 | { 132 | keysValues: []interface{}{"pod", klog.KRef("", "kubedns"), "status", "ready"}, 133 | want: " pod=\"kubedns\" status=\"ready\"", 134 | }, 135 | { 136 | keysValues: []interface{}{"pod", klog.KObj(test.KMetadataMock{Name: "test-name", NS: "test-ns"}), "status", "ready"}, 137 | want: " pod=\"test-ns/test-name\" status=\"ready\"", 138 | }, 139 | { 140 | keysValues: []interface{}{"pod", klog.KObj(test.KMetadataMock{Name: "test-name", NS: ""}), "status", "ready"}, 141 | want: " pod=\"test-name\" status=\"ready\"", 142 | }, 143 | { 144 | keysValues: []interface{}{"pod", klog.KObj(nil), "status", "ready"}, 145 | want: " pod=\"\" status=\"ready\"", 146 | }, 147 | { 148 | keysValues: []interface{}{"pod", klog.KObj((*test.PtrKMetadataMock)(nil)), "status", "ready"}, 149 | want: " pod=\"\" status=\"ready\"", 150 | }, 151 | { 152 | keysValues: []interface{}{"pod", klog.KObj((*test.KMetadataMock)(nil)), "status", "ready"}, 153 | want: " pod=\"\" status=\"ready\"", 154 | }, 155 | { 156 | keysValues: []interface{}{"pods", klog.KObjs([]test.KMetadataMock{ 157 | { 158 | Name: "kube-dns", 159 | NS: "kube-system", 160 | }, 161 | { 162 | Name: "mi-conf", 163 | }, 164 | })}, 165 | want: ` pods=[{"name":"kube-dns","namespace":"kube-system"},{"name":"mi-conf"}]`, 166 | }, 167 | { 168 | keysValues: []interface{}{"point-1", point{100, 200}, "point-2", emptyPoint}, 169 | want: " point-1=\"x=100, y=200\" point-2=\"\"", 170 | }, 171 | { 172 | keysValues: []interface{}{struct{ key string }{key: "k1"}, "value"}, 173 | want: " {k1}=\"value\"", 174 | }, 175 | { 176 | keysValues: []interface{}{1, "test"}, 177 | want: " %!s(int=1)=\"test\"", 178 | }, 179 | { 180 | keysValues: []interface{}{map[string]string{"k": "key"}, "value"}, 181 | want: " map[k:key]=\"value\"", 182 | }, 183 | } 184 | 185 | for _, d := range testKVList { 186 | b := &bytes.Buffer{} 187 | serialize.FormatKVs(b, d.keysValues) 188 | if b.String() != d.want { 189 | t.Errorf("KVListFormat error:\n got:\n\t%s\nwant:\t%s", b.String(), d.want) 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /internal/severity/severity.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All Rights Reserved. 2 | // Copyright 2022 The Kubernetes Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | // Package severity provides definitions for klog severity (info, warning, ...) 17 | package severity 18 | 19 | import ( 20 | "strings" 21 | ) 22 | 23 | // severity identifies the sort of log: info, warning etc. The binding to flag.Value 24 | // is handled in klog.go 25 | type Severity int32 // sync/atomic int32 26 | 27 | // These constants identify the log levels in order of increasing severity. 28 | // A message written to a high-severity log file is also written to each 29 | // lower-severity log file. 30 | const ( 31 | InfoLog Severity = iota 32 | WarningLog 33 | ErrorLog 34 | FatalLog 35 | NumSeverity = 4 36 | ) 37 | 38 | // Char contains one shortcut letter per severity level. 39 | const Char = "IWEF" 40 | 41 | // Name contains one name per severity level. 42 | var Name = []string{ 43 | InfoLog: "INFO", 44 | WarningLog: "WARNING", 45 | ErrorLog: "ERROR", 46 | FatalLog: "FATAL", 47 | } 48 | 49 | // ByName looks up a severity level by name. 50 | func ByName(s string) (Severity, bool) { 51 | s = strings.ToUpper(s) 52 | for i, name := range Name { 53 | if name == s { 54 | return Severity(i), true 55 | } 56 | } 57 | return 0, false 58 | } 59 | -------------------------------------------------------------------------------- /internal/sloghandler/sloghandler_slog.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package sloghandler 21 | 22 | import ( 23 | "context" 24 | "log/slog" 25 | "runtime" 26 | "strings" 27 | "time" 28 | 29 | "k8s.io/klog/v2/internal/severity" 30 | ) 31 | 32 | func Handle(_ context.Context, record slog.Record, groups string, printWithInfos func(file string, line int, now time.Time, err error, s severity.Severity, msg string, kvList []interface{})) error { 33 | now := record.Time 34 | if now.IsZero() { 35 | // This format doesn't support printing entries without a time. 36 | now = time.Now() 37 | } 38 | 39 | // slog has numeric severity levels, with 0 as default "info", negative for debugging, and 40 | // positive with some pre-defined levels for more important. Those ranges get mapped to 41 | // the corresponding klog levels where possible, with "info" the default that is used 42 | // also for negative debug levels. 43 | level := record.Level 44 | s := severity.InfoLog 45 | switch { 46 | case level >= slog.LevelError: 47 | s = severity.ErrorLog 48 | case level >= slog.LevelWarn: 49 | s = severity.WarningLog 50 | } 51 | 52 | var file string 53 | var line int 54 | if record.PC != 0 { 55 | // Same as https://cs.opensource.google/go/x/exp/+/642cacee:slog/record.go;drc=642cacee5cc05231f45555a333d07f1005ffc287;l=70 56 | fs := runtime.CallersFrames([]uintptr{record.PC}) 57 | f, _ := fs.Next() 58 | if f.File != "" { 59 | file = f.File 60 | if slash := strings.LastIndex(file, "/"); slash >= 0 { 61 | file = file[slash+1:] 62 | } 63 | line = f.Line 64 | } 65 | } else { 66 | file = "???" 67 | line = 1 68 | } 69 | 70 | kvList := make([]interface{}, 0, 2*record.NumAttrs()) 71 | record.Attrs(func(attr slog.Attr) bool { 72 | kvList = appendAttr(groups, kvList, attr) 73 | return true 74 | }) 75 | 76 | printWithInfos(file, line, now, nil, s, record.Message, kvList) 77 | return nil 78 | } 79 | 80 | func Attrs2KVList(groups string, attrs []slog.Attr) []interface{} { 81 | kvList := make([]interface{}, 0, 2*len(attrs)) 82 | for _, attr := range attrs { 83 | kvList = appendAttr(groups, kvList, attr) 84 | } 85 | return kvList 86 | } 87 | 88 | func appendAttr(groups string, kvList []interface{}, attr slog.Attr) []interface{} { 89 | var key string 90 | if groups != "" { 91 | key = groups + "." + attr.Key 92 | } else { 93 | key = attr.Key 94 | } 95 | return append(kvList, key, attr.Value) 96 | } 97 | -------------------------------------------------------------------------------- /internal/test/mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package test contains common code for klog tests. 18 | package test 19 | 20 | type KMetadataMock struct { 21 | Name, NS string 22 | } 23 | 24 | func (m KMetadataMock) GetName() string { 25 | return m.Name 26 | } 27 | func (m KMetadataMock) GetNamespace() string { 28 | return m.NS 29 | } 30 | 31 | type PtrKMetadataMock struct { 32 | Name, NS string 33 | } 34 | 35 | func (m *PtrKMetadataMock) GetName() string { 36 | return m.Name 37 | } 38 | func (m *PtrKMetadataMock) GetNamespace() string { 39 | return m.NS 40 | } 41 | -------------------------------------------------------------------------------- /internal/test/require/require.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package require 18 | 19 | import "testing" 20 | 21 | func NoError(tb testing.TB, err error) { 22 | if err != nil { 23 | tb.Helper() 24 | tb.Fatalf("Unexpected error: %v", err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/verbosity/helper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package verbosity 18 | 19 | func enabledInHelper(vs *VState, l Level) bool { 20 | return vs.Enabled(l, 0) 21 | } 22 | -------------------------------------------------------------------------------- /internal/verbosity/verbosity_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Google Inc. All Rights Reserved. 3 | Copyright 2022 The Kubernetes Authors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package verbosity 19 | 20 | import ( 21 | "testing" 22 | 23 | "k8s.io/klog/v2/internal/test/require" 24 | ) 25 | 26 | func TestV(t *testing.T) { 27 | vs := New() 28 | require.NoError(t, vs.verbosity.Set("2")) 29 | depth := 0 30 | if !vs.Enabled(1, depth) { 31 | t.Error("not enabled for 1") 32 | } 33 | if !vs.Enabled(2, depth) { 34 | t.Error("not enabled for 2") 35 | } 36 | if vs.Enabled(3, depth) { 37 | t.Error("enabled for 3") 38 | } 39 | } 40 | 41 | func TestVmoduleOn(t *testing.T) { 42 | vs := New() 43 | require.NoError(t, vs.vmodule.Set("verbosity_test=2")) 44 | depth := 0 45 | if !vs.Enabled(1, depth) { 46 | t.Error("not enabled for 1") 47 | } 48 | if !vs.Enabled(2, depth) { 49 | t.Error("not enabled for 2") 50 | } 51 | if vs.Enabled(3, depth) { 52 | t.Error("enabled for 3") 53 | } 54 | if enabledInHelper(vs, 1) { 55 | t.Error("enabled for helper at 1") 56 | } 57 | if enabledInHelper(vs, 2) { 58 | t.Error("enabled for helper at 2") 59 | } 60 | if enabledInHelper(vs, 3) { 61 | t.Error("enabled for helper at 3") 62 | } 63 | } 64 | 65 | // vGlobs are patterns that match/don't match this file at V=2. 66 | var vGlobs = map[string]bool{ 67 | // Easy to test the numeric match here. 68 | "verbosity_test=1": false, // If -vmodule sets V to 1, V(2) will fail. 69 | "verbosity_test=2": true, 70 | "verbosity_test=3": true, // If -vmodule sets V to 1, V(3) will succeed. 71 | // These all use 2 and check the patterns. All are true. 72 | "*=2": true, 73 | "?e*=2": true, 74 | "?????????_*=2": true, 75 | "??[arx]??????_*t=2": true, 76 | // These all use 2 and check the patterns. All are false. 77 | "*x=2": false, 78 | "m*=2": false, 79 | "??_*=2": false, 80 | "?[abc]?_*t=2": false, 81 | } 82 | 83 | // Test that vmodule globbing works as advertised. 84 | func testVmoduleGlob(pat string, match bool, t *testing.T) { 85 | vs := New() 86 | require.NoError(t, vs.vmodule.Set(pat)) 87 | depth := 0 88 | actual := vs.Enabled(2, depth) 89 | if actual != match { 90 | t.Errorf("incorrect match for %q: got %#v expected %#v", pat, actual, match) 91 | } 92 | } 93 | 94 | // Test that a vmodule globbing works as advertised. 95 | func TestVmoduleGlob(t *testing.T) { 96 | for glob, match := range vGlobs { 97 | testVmoduleGlob(glob, match, t) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /k8s_references.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klog 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "reflect" 23 | "strings" 24 | 25 | "github.com/go-logr/logr" 26 | ) 27 | 28 | // ObjectRef references a kubernetes object 29 | type ObjectRef struct { 30 | Name string `json:"name"` 31 | Namespace string `json:"namespace,omitempty"` 32 | } 33 | 34 | func (ref ObjectRef) String() string { 35 | if ref.Namespace != "" { 36 | var builder strings.Builder 37 | builder.Grow(len(ref.Namespace) + len(ref.Name) + 1) 38 | builder.WriteString(ref.Namespace) 39 | builder.WriteRune('/') 40 | builder.WriteString(ref.Name) 41 | return builder.String() 42 | } 43 | return ref.Name 44 | } 45 | 46 | func (ref ObjectRef) WriteText(out *bytes.Buffer) { 47 | out.WriteRune('"') 48 | ref.writeUnquoted(out) 49 | out.WriteRune('"') 50 | } 51 | 52 | func (ref ObjectRef) writeUnquoted(out *bytes.Buffer) { 53 | if ref.Namespace != "" { 54 | out.WriteString(ref.Namespace) 55 | out.WriteRune('/') 56 | } 57 | out.WriteString(ref.Name) 58 | } 59 | 60 | // MarshalLog ensures that loggers with support for structured output will log 61 | // as a struct by removing the String method via a custom type. 62 | func (ref ObjectRef) MarshalLog() interface{} { 63 | type or ObjectRef 64 | return or(ref) 65 | } 66 | 67 | var _ logr.Marshaler = ObjectRef{} 68 | 69 | // KMetadata is a subset of the kubernetes k8s.io/apimachinery/pkg/apis/meta/v1.Object interface 70 | // this interface may expand in the future, but will always be a subset of the 71 | // kubernetes k8s.io/apimachinery/pkg/apis/meta/v1.Object interface 72 | type KMetadata interface { 73 | GetName() string 74 | GetNamespace() string 75 | } 76 | 77 | // KObj returns ObjectRef from ObjectMeta 78 | func KObj(obj KMetadata) ObjectRef { 79 | if obj == nil { 80 | return ObjectRef{} 81 | } 82 | if val := reflect.ValueOf(obj); val.Kind() == reflect.Ptr && val.IsNil() { 83 | return ObjectRef{} 84 | } 85 | 86 | return ObjectRef{ 87 | Name: obj.GetName(), 88 | Namespace: obj.GetNamespace(), 89 | } 90 | } 91 | 92 | // KRef returns ObjectRef from name and namespace 93 | func KRef(namespace, name string) ObjectRef { 94 | return ObjectRef{ 95 | Name: name, 96 | Namespace: namespace, 97 | } 98 | } 99 | 100 | // KObjs returns slice of ObjectRef from an slice of ObjectMeta 101 | // 102 | // DEPRECATED: Use KObjSlice instead, it has better performance. 103 | func KObjs(arg interface{}) []ObjectRef { 104 | s := reflect.ValueOf(arg) 105 | if s.Kind() != reflect.Slice { 106 | return nil 107 | } 108 | objectRefs := make([]ObjectRef, 0, s.Len()) 109 | for i := 0; i < s.Len(); i++ { 110 | if v, ok := s.Index(i).Interface().(KMetadata); ok { 111 | objectRefs = append(objectRefs, KObj(v)) 112 | } else { 113 | return nil 114 | } 115 | } 116 | return objectRefs 117 | } 118 | 119 | // KObjSlice takes a slice of objects that implement the KMetadata interface 120 | // and returns an object that gets logged as a slice of ObjectRef values or a 121 | // string containing those values, depending on whether the logger prefers text 122 | // output or structured output. 123 | // 124 | // An error string is logged when KObjSlice is not passed a suitable slice. 125 | // 126 | // Processing of the argument is delayed until the value actually gets logged, 127 | // in contrast to KObjs where that overhead is incurred regardless of whether 128 | // the result is needed. 129 | func KObjSlice(arg interface{}) interface{} { 130 | return kobjSlice{arg: arg} 131 | } 132 | 133 | type kobjSlice struct { 134 | arg interface{} 135 | } 136 | 137 | var _ fmt.Stringer = kobjSlice{} 138 | var _ logr.Marshaler = kobjSlice{} 139 | 140 | func (ks kobjSlice) String() string { 141 | objectRefs, errStr := ks.process() 142 | if errStr != "" { 143 | return errStr 144 | } 145 | return fmt.Sprintf("%v", objectRefs) 146 | } 147 | 148 | func (ks kobjSlice) MarshalLog() interface{} { 149 | objectRefs, errStr := ks.process() 150 | if errStr != "" { 151 | return errStr 152 | } 153 | return objectRefs 154 | } 155 | 156 | func (ks kobjSlice) process() (objs []interface{}, err string) { 157 | s := reflect.ValueOf(ks.arg) 158 | switch s.Kind() { 159 | case reflect.Invalid: 160 | // nil parameter, print as nil. 161 | return nil, "" 162 | case reflect.Slice: 163 | // Okay, handle below. 164 | default: 165 | return nil, fmt.Sprintf("", ks.arg) 166 | } 167 | objectRefs := make([]interface{}, 0, s.Len()) 168 | for i := 0; i < s.Len(); i++ { 169 | item := s.Index(i).Interface() 170 | if item == nil { 171 | objectRefs = append(objectRefs, nil) 172 | } else if v, ok := item.(KMetadata); ok { 173 | objectRefs = append(objectRefs, KObj(v)) 174 | } else { 175 | return nil, fmt.Sprintf("", item) 176 | } 177 | } 178 | return objectRefs, "" 179 | } 180 | 181 | var nilToken = []byte("null") 182 | 183 | func (ks kobjSlice) WriteText(out *bytes.Buffer) { 184 | s := reflect.ValueOf(ks.arg) 185 | switch s.Kind() { 186 | case reflect.Invalid: 187 | // nil parameter, print as null. 188 | out.Write(nilToken) 189 | return 190 | case reflect.Slice: 191 | // Okay, handle below. 192 | default: 193 | fmt.Fprintf(out, `""`, ks.arg) 194 | return 195 | } 196 | out.Write([]byte{'['}) 197 | defer out.Write([]byte{']'}) 198 | for i := 0; i < s.Len(); i++ { 199 | if i > 0 { 200 | out.Write([]byte{','}) 201 | } 202 | item := s.Index(i).Interface() 203 | if item == nil { 204 | out.Write(nilToken) 205 | } else if v, ok := item.(KMetadata); ok { 206 | KObj(v).WriteText(out) 207 | } else { 208 | fmt.Fprintf(out, `""`, item) 209 | return 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /k8s_references_slog.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2021 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package klog 21 | 22 | import ( 23 | "log/slog" 24 | ) 25 | 26 | func (ref ObjectRef) LogValue() slog.Value { 27 | if ref.Namespace != "" { 28 | return slog.GroupValue(slog.String("name", ref.Name), slog.String("namespace", ref.Namespace)) 29 | } 30 | return slog.GroupValue(slog.String("name", ref.Name)) 31 | } 32 | 33 | var _ slog.LogValuer = ObjectRef{} 34 | 35 | func (ks kobjSlice) LogValue() slog.Value { 36 | return slog.AnyValue(ks.MarshalLog()) 37 | } 38 | 39 | var _ slog.LogValuer = kobjSlice{} 40 | -------------------------------------------------------------------------------- /klog_file.go: -------------------------------------------------------------------------------- 1 | // Go support for leveled logs, analogous to https://code.google.com/p/google-glog/ 2 | // 3 | // Copyright 2013 Google Inc. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | // File I/O for logs. 18 | 19 | package klog 20 | 21 | import ( 22 | "errors" 23 | "fmt" 24 | "os" 25 | "path/filepath" 26 | "strings" 27 | "sync" 28 | "time" 29 | ) 30 | 31 | // MaxSize is the maximum size of a log file in bytes. 32 | var MaxSize uint64 = 1024 * 1024 * 1800 33 | 34 | // logDirs lists the candidate directories for new log files. 35 | var logDirs []string 36 | 37 | func createLogDirs() { 38 | if logging.logDir != "" { 39 | logDirs = append(logDirs, logging.logDir) 40 | } 41 | logDirs = append(logDirs, os.TempDir()) 42 | } 43 | 44 | var ( 45 | pid = os.Getpid() 46 | program = filepath.Base(os.Args[0]) 47 | host = "unknownhost" 48 | userName = "unknownuser" 49 | userNameOnce sync.Once 50 | ) 51 | 52 | func init() { 53 | if h, err := os.Hostname(); err == nil { 54 | host = shortHostname(h) 55 | } 56 | } 57 | 58 | // shortHostname returns its argument, truncating at the first period. 59 | // For instance, given "www.google.com" it returns "www". 60 | func shortHostname(hostname string) string { 61 | if i := strings.Index(hostname, "."); i >= 0 { 62 | return hostname[:i] 63 | } 64 | return hostname 65 | } 66 | 67 | // logName returns a new log file name containing tag, with start time t, and 68 | // the name for the symlink for tag. 69 | func logName(tag string, t time.Time) (name, link string) { 70 | name = fmt.Sprintf("%s.%s.%s.log.%s.%04d%02d%02d-%02d%02d%02d.%d", 71 | program, 72 | host, 73 | getUserName(), 74 | tag, 75 | t.Year(), 76 | t.Month(), 77 | t.Day(), 78 | t.Hour(), 79 | t.Minute(), 80 | t.Second(), 81 | pid) 82 | return name, program + "." + tag 83 | } 84 | 85 | var onceLogDirs sync.Once 86 | 87 | // create creates a new log file and returns the file and its filename, which 88 | // contains tag ("INFO", "FATAL", etc.) and t. If the file is created 89 | // successfully, create also attempts to update the symlink for that tag, ignoring 90 | // errors. 91 | // The startup argument indicates whether this is the initial startup of klog. 92 | // If startup is true, existing files are opened for appending instead of truncated. 93 | func create(tag string, t time.Time, startup bool) (f *os.File, filename string, err error) { 94 | if logging.logFile != "" { 95 | f, err := openOrCreate(logging.logFile, startup) 96 | if err == nil { 97 | return f, logging.logFile, nil 98 | } 99 | return nil, "", fmt.Errorf("log: unable to create log: %v", err) 100 | } 101 | onceLogDirs.Do(createLogDirs) 102 | if len(logDirs) == 0 { 103 | return nil, "", errors.New("log: no log dirs") 104 | } 105 | name, link := logName(tag, t) 106 | var lastErr error 107 | for _, dir := range logDirs { 108 | fname := filepath.Join(dir, name) 109 | f, err := openOrCreate(fname, startup) 110 | if err == nil { 111 | symlink := filepath.Join(dir, link) 112 | _ = os.Remove(symlink) // ignore err 113 | _ = os.Symlink(name, symlink) // ignore err 114 | return f, fname, nil 115 | } 116 | lastErr = err 117 | } 118 | return nil, "", fmt.Errorf("log: cannot create log: %v", lastErr) 119 | } 120 | 121 | // The startup argument indicates whether this is the initial startup of klog. 122 | // If startup is true, existing files are opened for appending instead of truncated. 123 | func openOrCreate(name string, startup bool) (*os.File, error) { 124 | if startup { 125 | f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 126 | return f, err 127 | } 128 | f, err := os.Create(name) 129 | return f, err 130 | } 131 | -------------------------------------------------------------------------------- /klog_file_others.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package klog 5 | 6 | import ( 7 | "os/user" 8 | ) 9 | 10 | func getUserName() string { 11 | userNameOnce.Do(func() { 12 | current, err := user.Current() 13 | if err == nil { 14 | userName = current.Username 15 | } 16 | }) 17 | 18 | return userName 19 | } 20 | -------------------------------------------------------------------------------- /klog_file_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package klog 5 | 6 | import ( 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func getUserName() string { 12 | userNameOnce.Do(func() { 13 | // On Windows, the Go 'user' package requires netapi32.dll. 14 | // This affects Windows Nano Server: 15 | // https://github.com/golang/go/issues/21867 16 | // Fallback to using environment variables. 17 | u := os.Getenv("USERNAME") 18 | if len(u) == 0 { 19 | return 20 | } 21 | // Sanitize the USERNAME since it may contain filepath separators. 22 | u = strings.Replace(u, `\`, "_", -1) 23 | 24 | // user.Current().Username normally produces something like 'USERDOMAIN\USERNAME' 25 | d := os.Getenv("USERDOMAIN") 26 | if len(d) != 0 { 27 | userName = d + "_" + u 28 | } else { 29 | userName = u 30 | } 31 | }) 32 | 33 | return userName 34 | } 35 | -------------------------------------------------------------------------------- /klog_wrappers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package klog 16 | 17 | // These helper functions must be in a separate source file because the 18 | // tests in klog_test.go compare the logged source code file name against 19 | // "klog_test.go". "klog_wrappers_test.go" must *not* be logged. 20 | 21 | func myInfoS(msg string, keyAndValues ...interface{}) { 22 | InfoSDepth(1, msg, keyAndValues...) 23 | } 24 | 25 | func myErrorS(err error, msg string, keyAndValues ...interface{}) { 26 | ErrorSDepth(1, err, msg, keyAndValues...) 27 | } 28 | -------------------------------------------------------------------------------- /klogr.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klog 18 | 19 | import ( 20 | "github.com/go-logr/logr" 21 | 22 | "k8s.io/klog/v2/internal/serialize" 23 | ) 24 | 25 | const ( 26 | // nameKey is used to log the `WithName` values as an additional attribute. 27 | nameKey = "logger" 28 | ) 29 | 30 | // NewKlogr returns a logger that is functionally identical to 31 | // klogr.NewWithOptions(klogr.FormatKlog), i.e. it passes through to klog. The 32 | // difference is that it uses a simpler implementation. 33 | func NewKlogr() Logger { 34 | return New(&klogger{}) 35 | } 36 | 37 | // klogger is a subset of klogr/klogr.go. It had to be copied to break an 38 | // import cycle (klogr wants to use klog, and klog wants to use klogr). 39 | type klogger struct { 40 | callDepth int 41 | 42 | // hasPrefix is true if the first entry in values is the special 43 | // nameKey key/value. Such an entry gets added and later updated in 44 | // WithName. 45 | hasPrefix bool 46 | 47 | values []interface{} 48 | groups string 49 | } 50 | 51 | func (l *klogger) Init(info logr.RuntimeInfo) { 52 | l.callDepth += info.CallDepth 53 | } 54 | 55 | func (l *klogger) Info(level int, msg string, kvList ...interface{}) { 56 | merged := serialize.WithValues(l.values, kvList) 57 | // Skip this function. 58 | VDepth(l.callDepth+1, Level(level)).InfoSDepth(l.callDepth+1, msg, merged...) 59 | } 60 | 61 | func (l *klogger) Enabled(level int) bool { 62 | return VDepth(l.callDepth+1, Level(level)).Enabled() 63 | } 64 | 65 | func (l *klogger) Error(err error, msg string, kvList ...interface{}) { 66 | merged := serialize.WithValues(l.values, kvList) 67 | ErrorSDepth(l.callDepth+1, err, msg, merged...) 68 | } 69 | 70 | // WithName returns a new logr.Logger with the specified name appended. klogr 71 | // uses '.' characters to separate name elements. Callers should not pass '.' 72 | // in the provided name string, but this library does not actually enforce that. 73 | func (l klogger) WithName(name string) logr.LogSink { 74 | if l.hasPrefix { 75 | // Copy slice and modify value. No length checks and type 76 | // assertions are needed because hasPrefix is only true if the 77 | // first two elements exist and are key/value strings. 78 | v := make([]interface{}, 0, len(l.values)) 79 | v = append(v, l.values...) 80 | prefix, _ := v[1].(string) 81 | v[1] = prefix + "." + name 82 | l.values = v 83 | } else { 84 | // Preprend new key/value pair. 85 | v := make([]interface{}, 0, 2+len(l.values)) 86 | v = append(v, nameKey, name) 87 | v = append(v, l.values...) 88 | l.values = v 89 | l.hasPrefix = true 90 | } 91 | return &l 92 | } 93 | 94 | func (l klogger) WithValues(kvList ...interface{}) logr.LogSink { 95 | l.values = serialize.WithValues(l.values, kvList) 96 | return &l 97 | } 98 | 99 | func (l klogger) WithCallDepth(depth int) logr.LogSink { 100 | l.callDepth += depth 101 | return &l 102 | } 103 | 104 | var _ logr.LogSink = &klogger{} 105 | var _ logr.CallDepthLogSink = &klogger{} 106 | -------------------------------------------------------------------------------- /klogr/README.md: -------------------------------------------------------------------------------- 1 | # Minimal Go logging using klog 2 | 3 | This package implements the [logr interface](https://github.com/go-logr/logr) 4 | in terms of Kubernetes' [klog](https://github.com/kubernetes/klog). This 5 | provides a relatively minimalist API to logging in Go, backed by a well-proven 6 | implementation. 7 | 8 | Because klogr was implemented before klog itself added supported for 9 | structured logging, the default in klogr is to serialize key/value 10 | pairs with JSON and log the result as text messages via klog. This 11 | does not work well when klog itself forwards output to a structured 12 | logger. 13 | 14 | Therefore the recommended approach is to let klogr pass all log 15 | messages through to klog and deal with structured logging there. Just 16 | beware that the output of klog without a structured logger is meant to 17 | be human-readable, in contrast to the JSON-based traditional format. 18 | 19 | This is a BETA grade implementation. 20 | -------------------------------------------------------------------------------- /klogr/calldepth-test/call_depth_helper_test.go: -------------------------------------------------------------------------------- 1 | package calldepth 2 | 3 | import ( 4 | "github.com/go-logr/logr" 5 | ) 6 | 7 | // Putting these functions into a separate file makes it possible to validate that 8 | // their source code file is *not* logged because of WithCallDepth(1). 9 | 10 | func myInfo(l logr.Logger, msg string) { 11 | l.WithCallDepth(2).Info(msg) 12 | } 13 | 14 | func myInfo2(l logr.Logger, msg string) { 15 | myInfo(l.WithCallDepth(2), msg) 16 | } 17 | -------------------------------------------------------------------------------- /klogr/calldepth-test/call_depth_main_test.go: -------------------------------------------------------------------------------- 1 | // Package calldepth does black-box testing. 2 | // 3 | // Another intentional effect is that "go test" compiles 4 | // this into a separate binary which we need because 5 | // we have to configure klog differently that TestOutput. 6 | 7 | package calldepth 8 | 9 | import ( 10 | "bytes" 11 | "flag" 12 | "strings" 13 | "testing" 14 | 15 | "k8s.io/klog/v2" 16 | "k8s.io/klog/v2/internal/test/require" 17 | "k8s.io/klog/v2/klogr" 18 | ) 19 | 20 | func TestCallDepth(t *testing.T) { 21 | klog.InitFlags(nil) 22 | require.NoError(t, flag.CommandLine.Set("v", "10")) 23 | require.NoError(t, flag.CommandLine.Set("skip_headers", "false")) 24 | require.NoError(t, flag.CommandLine.Set("logtostderr", "false")) 25 | require.NoError(t, flag.CommandLine.Set("alsologtostderr", "false")) 26 | require.NoError(t, flag.CommandLine.Set("stderrthreshold", "10")) 27 | flag.Parse() 28 | 29 | t.Run("call-depth", func(t *testing.T) { 30 | logr := klogr.New() 31 | 32 | // hijack the klog output 33 | tmpWriteBuffer := bytes.NewBuffer(nil) 34 | klog.SetOutput(tmpWriteBuffer) 35 | 36 | validate := func(t *testing.T) { 37 | output := tmpWriteBuffer.String() 38 | if !strings.Contains(output, "call_depth_main_test.go:") { 39 | t.Fatalf("output should have contained call_depth_main_test.go, got instead: %s", output) 40 | } 41 | } 42 | 43 | t.Run("direct", func(t *testing.T) { 44 | logr.Info("hello world") 45 | validate(t) 46 | }) 47 | 48 | t.Run("indirect", func(t *testing.T) { 49 | myInfo(logr, "hello world") 50 | validate(t) 51 | }) 52 | 53 | t.Run("nested", func(t *testing.T) { 54 | myInfo2(logr, "hello world") 55 | validate(t) 56 | }) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /klogr/klogr.go: -------------------------------------------------------------------------------- 1 | // Package klogr implements github.com/go-logr/logr.Logger in terms of 2 | // k8s.io/klog. 3 | package klogr 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "fmt" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/go-logr/logr" 13 | 14 | "k8s.io/klog/v2" 15 | "k8s.io/klog/v2/internal/serialize" 16 | ) 17 | 18 | const ( 19 | // nameKey is used to log the `WithName` values as an additional attribute. 20 | nameKey = "logger" 21 | ) 22 | 23 | // Option is a functional option that reconfigures the logger created with New. 24 | type Option func(*klogger) 25 | 26 | // Format defines how log output is produced. 27 | type Format string 28 | 29 | const ( 30 | // FormatSerialize tells klogr to turn key/value pairs into text itself 31 | // before invoking klog. Key/value pairs are sorted by key. 32 | FormatSerialize Format = "Serialize" 33 | 34 | // FormatKlog tells klogr to pass all text messages and key/value pairs 35 | // directly to klog. Klog itself then serializes in a human-readable 36 | // format and optionally passes on to a structure logging backend. 37 | FormatKlog Format = "Klog" 38 | ) 39 | 40 | // WithFormat selects the output format. 41 | func WithFormat(format Format) Option { 42 | return func(l *klogger) { 43 | l.format = format 44 | } 45 | } 46 | 47 | // New returns a logr.Logger which serializes output itself 48 | // and writes it via klog. 49 | // 50 | // Deprecated: this uses a custom, out-dated output format. Use textlogger.NewLogger instead. 51 | func New() logr.Logger { 52 | return NewWithOptions(WithFormat(FormatSerialize)) 53 | } 54 | 55 | // NewWithOptions returns a logr.Logger which serializes as determined 56 | // by the WithFormat option and writes via klog. The default is 57 | // FormatKlog. 58 | // 59 | // Deprecated: FormatSerialize is out-dated. For FormatKlog, use textlogger.NewLogger instead. 60 | func NewWithOptions(options ...Option) logr.Logger { 61 | l := klogger{ 62 | level: 0, 63 | values: nil, 64 | format: FormatKlog, 65 | } 66 | for _, option := range options { 67 | option(&l) 68 | } 69 | return logr.New(&l) 70 | } 71 | 72 | type klogger struct { 73 | level int 74 | callDepth int 75 | 76 | // hasPrefix is true if the first entry in values is the special 77 | // nameKey key/value. Such an entry gets added and later updated in 78 | // WithName. 79 | hasPrefix bool 80 | 81 | values []interface{} 82 | format Format 83 | } 84 | 85 | func (l *klogger) Init(info logr.RuntimeInfo) { 86 | l.callDepth += info.CallDepth 87 | } 88 | 89 | func flatten(kvList ...interface{}) string { 90 | keys := make([]string, 0, len(kvList)) 91 | vals := make(map[string]interface{}, len(kvList)) 92 | for i := 0; i < len(kvList); i += 2 { 93 | k, ok := kvList[i].(string) 94 | if !ok { 95 | panic(fmt.Sprintf("key is not a string: %s", pretty(kvList[i]))) 96 | } 97 | var v interface{} 98 | if i+1 < len(kvList) { 99 | v = kvList[i+1] 100 | } 101 | // Only print each key once... 102 | if _, seen := vals[k]; !seen { 103 | keys = append(keys, k) 104 | } 105 | // ... with the latest value. 106 | vals[k] = v 107 | } 108 | sort.Strings(keys) 109 | buf := bytes.Buffer{} 110 | for i, k := range keys { 111 | v := vals[k] 112 | if i > 0 { 113 | buf.WriteRune(' ') 114 | } 115 | buf.WriteString(pretty(k)) 116 | buf.WriteString("=") 117 | buf.WriteString(pretty(v)) 118 | } 119 | return buf.String() 120 | } 121 | 122 | func pretty(value interface{}) string { 123 | if err, ok := value.(error); ok { 124 | if _, ok := value.(json.Marshaler); !ok { 125 | value = err.Error() 126 | } 127 | } 128 | buffer := &bytes.Buffer{} 129 | encoder := json.NewEncoder(buffer) 130 | encoder.SetEscapeHTML(false) 131 | if err := encoder.Encode(value); err != nil { 132 | return fmt.Sprintf("<>", err) 133 | } 134 | return strings.TrimSpace(buffer.String()) 135 | } 136 | 137 | func (l *klogger) Info(level int, msg string, kvList ...interface{}) { 138 | switch l.format { 139 | case FormatSerialize: 140 | msgStr := flatten("msg", msg) 141 | merged := serialize.WithValues(l.values, kvList) 142 | kvStr := flatten(merged...) 143 | klog.VDepth(l.callDepth+1, klog.Level(level)).InfoDepth(l.callDepth+1, msgStr, " ", kvStr) 144 | case FormatKlog: 145 | merged := serialize.WithValues(l.values, kvList) 146 | klog.VDepth(l.callDepth+1, klog.Level(level)).InfoSDepth(l.callDepth+1, msg, merged...) 147 | } 148 | } 149 | 150 | func (l *klogger) Enabled(level int) bool { 151 | return klog.VDepth(l.callDepth+1, klog.Level(level)).Enabled() 152 | } 153 | 154 | func (l *klogger) Error(err error, msg string, kvList ...interface{}) { 155 | msgStr := flatten("msg", msg) 156 | var loggableErr interface{} 157 | if err != nil { 158 | loggableErr = serialize.ErrorToString(err) 159 | } 160 | switch l.format { 161 | case FormatSerialize: 162 | errStr := flatten("error", loggableErr) 163 | merged := serialize.WithValues(l.values, kvList) 164 | kvStr := flatten(merged...) 165 | klog.ErrorDepth(l.callDepth+1, msgStr, " ", errStr, " ", kvStr) 166 | case FormatKlog: 167 | merged := serialize.WithValues(l.values, kvList) 168 | klog.ErrorSDepth(l.callDepth+1, err, msg, merged...) 169 | } 170 | } 171 | 172 | // WithName returns a new logr.Logger with the specified name appended. klogr 173 | // uses '.' characters to separate name elements. Callers should not pass '.' 174 | // in the provided name string, but this library does not actually enforce that. 175 | func (l klogger) WithName(name string) logr.LogSink { 176 | if l.hasPrefix { 177 | // Copy slice and modify value. No length checks and type 178 | // assertions are needed because hasPrefix is only true if the 179 | // first two elements exist and are key/value strings. 180 | v := make([]interface{}, 0, len(l.values)) 181 | v = append(v, l.values...) 182 | prefix, _ := v[1].(string) 183 | prefix = prefix + "." + name 184 | v[1] = prefix 185 | l.values = v 186 | } else { 187 | // Preprend new key/value pair. 188 | v := make([]interface{}, 0, 2+len(l.values)) 189 | v = append(v, nameKey, name) 190 | v = append(v, l.values...) 191 | l.values = v 192 | l.hasPrefix = true 193 | } 194 | return &l 195 | } 196 | 197 | func (l klogger) WithValues(kvList ...interface{}) logr.LogSink { 198 | l.values = serialize.WithValues(l.values, kvList) 199 | return &l 200 | } 201 | 202 | func (l klogger) WithCallDepth(depth int) logr.LogSink { 203 | l.callDepth += depth 204 | return &l 205 | } 206 | 207 | var _ logr.LogSink = &klogger{} 208 | var _ logr.CallDepthLogSink = &klogger{} 209 | -------------------------------------------------------------------------------- /klogr/klogr_test.go: -------------------------------------------------------------------------------- 1 | package klogr 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "strings" 8 | "testing" 9 | 10 | "k8s.io/klog/v2" 11 | "k8s.io/klog/v2/internal/test/require" 12 | "k8s.io/klog/v2/test" 13 | 14 | "github.com/go-logr/logr" 15 | ) 16 | 17 | const ( 18 | formatDefault = "Default" 19 | formatNew = "New" 20 | ) 21 | 22 | func testOutput(t *testing.T, format string) { 23 | createLogger := func() logr.Logger { 24 | switch format { 25 | case formatNew: 26 | return New() 27 | case formatDefault: 28 | return NewWithOptions() 29 | default: 30 | return NewWithOptions(WithFormat(Format(format))) 31 | } 32 | } 33 | tests := map[string]struct { 34 | klogr logr.Logger 35 | text string 36 | keysAndValues []interface{} 37 | err error 38 | expectedOutput string 39 | expectedKlogOutput string 40 | }{ 41 | "should log with values passed to keysAndValues": { 42 | klogr: createLogger().V(0), 43 | text: "test", 44 | keysAndValues: []interface{}{"akey", "avalue"}, 45 | expectedOutput: `"msg"="test" "akey"="avalue" 46 | `, 47 | expectedKlogOutput: `"test" akey="avalue" 48 | `, 49 | }, 50 | "should log with name and values passed to keysAndValues": { 51 | klogr: createLogger().V(0).WithName("me"), 52 | text: "test", 53 | keysAndValues: []interface{}{"akey", "avalue"}, 54 | // Sorted by keys. 55 | expectedOutput: `"msg"="test" "akey"="avalue" "logger"="me" 56 | `, 57 | // Not sorted by keys. 58 | expectedKlogOutput: `"test" logger="me" akey="avalue" 59 | `, 60 | }, 61 | "should log with multiple names and values passed to keysAndValues": { 62 | klogr: createLogger().V(0).WithName("hello").WithName("world"), 63 | text: "test", 64 | keysAndValues: []interface{}{"akey", "avalue"}, 65 | // Sorted by keys. 66 | expectedOutput: `"msg"="test" "akey"="avalue" "logger"="hello.world" 67 | `, 68 | // Not sorted by keys. 69 | expectedKlogOutput: `"test" logger="hello.world" akey="avalue" 70 | `, 71 | }, 72 | "de-duplicate keys with the same value": { 73 | klogr: createLogger().V(0), 74 | text: "test", 75 | keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue"}, 76 | expectedOutput: `"msg"="test" "akey"="avalue" 77 | `, 78 | expectedKlogOutput: `"test" akey="avalue" 79 | `, 80 | }, 81 | "de-duplicate keys when the values are passed to Info": { 82 | klogr: createLogger().V(0), 83 | text: "test", 84 | keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue2"}, 85 | expectedOutput: `"msg"="test" "akey"="avalue2" 86 | `, 87 | expectedKlogOutput: `"test" akey="avalue2" 88 | `, 89 | }, 90 | "should only print the duplicate key that is passed to Info if one was passed to the logger": { 91 | klogr: createLogger().WithValues("akey", "avalue"), 92 | text: "test", 93 | keysAndValues: []interface{}{"akey", "avalue"}, 94 | expectedOutput: `"msg"="test" "akey"="avalue" 95 | `, 96 | expectedKlogOutput: `"test" akey="avalue" 97 | `, 98 | }, 99 | "should sort within logger and parameter key/value pairs in the default format and dump the logger pairs first": { 100 | klogr: createLogger().WithValues("akey9", "avalue9", "akey8", "avalue8", "akey1", "avalue1"), 101 | text: "test", 102 | keysAndValues: []interface{}{"akey5", "avalue5", "akey4", "avalue4"}, 103 | expectedOutput: `"msg"="test" "akey1"="avalue1" "akey4"="avalue4" "akey5"="avalue5" "akey8"="avalue8" "akey9"="avalue9" 104 | `, 105 | expectedKlogOutput: `"test" akey9="avalue9" akey8="avalue8" akey1="avalue1" akey5="avalue5" akey4="avalue4" 106 | `, 107 | }, 108 | "should only print the key passed to Info when one is already set on the logger": { 109 | klogr: createLogger().WithValues("akey", "avalue"), 110 | text: "test", 111 | keysAndValues: []interface{}{"akey", "avalue2"}, 112 | expectedOutput: `"msg"="test" "akey"="avalue2" 113 | `, 114 | expectedKlogOutput: `"test" akey="avalue2" 115 | `, 116 | }, 117 | "should correctly handle odd-numbers of KVs": { 118 | klogr: createLogger(), 119 | text: "test", 120 | keysAndValues: []interface{}{"akey", "avalue", "akey2"}, 121 | expectedOutput: `"msg"="test" "akey"="avalue" "akey2"="(MISSING)" 122 | `, 123 | expectedKlogOutput: `"test" akey="avalue" akey2="(MISSING)" 124 | `, 125 | }, 126 | "should correctly handle odd-numbers of KVs in WithValue": { 127 | klogr: createLogger().WithValues("keyWithoutValue"), 128 | text: "test", 129 | keysAndValues: []interface{}{"akey", "avalue", "akey2"}, 130 | // klogr format sorts all key/value pairs. 131 | expectedOutput: `"msg"="test" "akey"="avalue" "akey2"="(MISSING)" "keyWithoutValue"="(MISSING)" 132 | `, 133 | expectedKlogOutput: `"test" keyWithoutValue="(MISSING)" akey="avalue" akey2="(MISSING)" 134 | `, 135 | }, 136 | "should correctly html characters": { 137 | klogr: createLogger(), 138 | text: "test", 139 | keysAndValues: []interface{}{"akey", "<&>"}, 140 | expectedOutput: `"msg"="test" "akey"="<&>" 141 | `, 142 | expectedKlogOutput: `"test" akey="<&>" 143 | `, 144 | }, 145 | "should correctly handle odd-numbers of KVs in both log values and Info args": { 146 | klogr: createLogger().WithValues("basekey1", "basevar1", "basekey2"), 147 | text: "test", 148 | keysAndValues: []interface{}{"akey", "avalue", "akey2"}, 149 | // klogr format sorts all key/value pairs. 150 | expectedOutput: `"msg"="test" "akey"="avalue" "akey2"="(MISSING)" "basekey1"="basevar1" "basekey2"="(MISSING)" 151 | `, 152 | expectedKlogOutput: `"test" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)" 153 | `, 154 | }, 155 | "should correctly print regular error types": { 156 | klogr: createLogger().V(0), 157 | text: "test", 158 | keysAndValues: []interface{}{"err", errors.New("whoops")}, 159 | expectedOutput: `"msg"="test" "err"="whoops" 160 | `, 161 | expectedKlogOutput: `"test" err="whoops" 162 | `, 163 | }, 164 | "should use MarshalJSON in the default format if an error type implements it": { 165 | klogr: createLogger().V(0), 166 | text: "test", 167 | keysAndValues: []interface{}{"err", &customErrorJSON{"whoops"}}, 168 | expectedOutput: `"msg"="test" "err"="WHOOPS" 169 | `, 170 | expectedKlogOutput: `"test" err="whoops" 171 | `, 172 | }, 173 | "should correctly print regular error types when using logr.Error": { 174 | klogr: createLogger().V(0), 175 | text: "test", 176 | err: errors.New("whoops"), 177 | expectedOutput: `"msg"="test" "error"="whoops" 178 | `, 179 | expectedKlogOutput: `"test" err="whoops" 180 | `, 181 | }, 182 | } 183 | for n, test := range tests { 184 | t.Run(n, func(t *testing.T) { 185 | 186 | // hijack the klog output 187 | tmpWriteBuffer := bytes.NewBuffer(nil) 188 | klog.SetOutput(tmpWriteBuffer) 189 | 190 | if test.err != nil { 191 | test.klogr.Error(test.err, test.text, test.keysAndValues...) 192 | } else { 193 | test.klogr.Info(test.text, test.keysAndValues...) 194 | } 195 | 196 | // call Flush to ensure the text isn't still buffered 197 | klog.Flush() 198 | 199 | actual := tmpWriteBuffer.String() 200 | expectedOutput := test.expectedOutput 201 | if format == string(FormatKlog) || format == formatDefault { 202 | expectedOutput = test.expectedKlogOutput 203 | } 204 | if actual != expectedOutput { 205 | t.Errorf("Expected:\n%s\nActual:\n%s\n", expectedOutput, actual) 206 | } 207 | }) 208 | } 209 | } 210 | 211 | func TestOutput(t *testing.T) { 212 | fs := test.InitKlog(t) 213 | require.NoError(t, fs.Set("skip_headers", "true")) 214 | 215 | formats := []string{ 216 | formatNew, 217 | formatDefault, 218 | string(FormatSerialize), 219 | string(FormatKlog), 220 | } 221 | for _, format := range formats { 222 | t.Run(format, func(t *testing.T) { 223 | testOutput(t, format) 224 | }) 225 | } 226 | } 227 | 228 | type customErrorJSON struct { 229 | s string 230 | } 231 | 232 | func (e *customErrorJSON) Error() string { 233 | return e.s 234 | } 235 | 236 | func (e *customErrorJSON) MarshalJSON() ([]byte, error) { 237 | return json.Marshal(strings.ToUpper(e.s)) 238 | } 239 | -------------------------------------------------------------------------------- /klogr/output_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klogr_test 18 | 19 | import ( 20 | "io" 21 | "testing" 22 | 23 | "github.com/go-logr/logr" 24 | 25 | "k8s.io/klog/v2/klogr" 26 | "k8s.io/klog/v2/test" 27 | ) 28 | 29 | // TestKlogrOutput tests klogr output via klog. 30 | func TestKlogrOutput(t *testing.T) { 31 | test.InitKlog(t) 32 | test.Output(t, test.OutputConfig{ 33 | NewLogger: func(_ io.Writer, _ int, _ string) logr.Logger { 34 | return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog)) 35 | }, 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /klogr_helper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klog_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "k8s.io/klog/v2" 23 | ) 24 | 25 | func testVerbosity(t *testing.T, logger klog.Logger) { 26 | // This runs with -v=5 -vmodule=klog_helper_test=10. 27 | logger.V(11).Info("v11 not visible from klogr_helper_test.go") 28 | if logger.V(11).Enabled() { 29 | t.Error("V(11).Enabled() in klogr_helper_test.go should have returned false.") 30 | } 31 | logger.V(10).Info("v10 visible from klogr_helper_test.go") 32 | if !logger.V(10).Enabled() { 33 | t.Error("V(10).Enabled() in klogr_helper_test.go should have returned true.") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /klogr_slog.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package klog 21 | 22 | import ( 23 | "context" 24 | "log/slog" 25 | "strconv" 26 | "time" 27 | 28 | "github.com/go-logr/logr" 29 | 30 | "k8s.io/klog/v2/internal/buffer" 31 | "k8s.io/klog/v2/internal/serialize" 32 | "k8s.io/klog/v2/internal/severity" 33 | "k8s.io/klog/v2/internal/sloghandler" 34 | ) 35 | 36 | func (l *klogger) Handle(ctx context.Context, record slog.Record) error { 37 | if logging.logger != nil { 38 | if slogSink, ok := logging.logger.GetSink().(logr.SlogSink); ok { 39 | // Let that logger do the work. 40 | return slogSink.Handle(ctx, record) 41 | } 42 | } 43 | 44 | return sloghandler.Handle(ctx, record, l.groups, slogOutput) 45 | } 46 | 47 | // slogOutput corresponds to several different functions in klog.go. 48 | // It goes through some of the same checks and formatting steps before 49 | // it ultimately converges by calling logging.printWithInfos. 50 | func slogOutput(file string, line int, now time.Time, err error, s severity.Severity, msg string, kvList []interface{}) { 51 | // See infoS. 52 | if logging.logger != nil { 53 | // Taking this path happens when klog has a logger installed 54 | // as backend which doesn't support slog. Not good, we have to 55 | // guess about the call depth and drop the actual location. 56 | logger := logging.logger.WithCallDepth(2) 57 | if s > severity.ErrorLog { 58 | logger.Error(err, msg, kvList...) 59 | } else { 60 | logger.Info(msg, kvList...) 61 | } 62 | return 63 | } 64 | 65 | // See printS. 66 | qMsg := make([]byte, 0, 1024) 67 | qMsg = strconv.AppendQuote(qMsg, msg) 68 | 69 | b := buffer.GetBuffer() 70 | b.Write(qMsg) 71 | 72 | var errKV []interface{} 73 | if err != nil { 74 | errKV = []interface{}{"err", err} 75 | } 76 | serialize.FormatKVs(&b.Buffer, errKV, kvList) 77 | 78 | // See print + header. 79 | buf := logging.formatHeader(s, file, line, now) 80 | logging.printWithInfos(buf, file, line, s, nil, nil, 0, &b.Buffer) 81 | 82 | buffer.PutBuffer(b) 83 | } 84 | 85 | func (l *klogger) WithAttrs(attrs []slog.Attr) logr.SlogSink { 86 | clone := *l 87 | clone.values = serialize.WithValues(l.values, sloghandler.Attrs2KVList(l.groups, attrs)) 88 | return &clone 89 | } 90 | 91 | func (l *klogger) WithGroup(name string) logr.SlogSink { 92 | clone := *l 93 | if clone.groups != "" { 94 | clone.groups += "." + name 95 | } else { 96 | clone.groups = name 97 | } 98 | return &clone 99 | } 100 | 101 | var _ logr.SlogSink = &klogger{} 102 | -------------------------------------------------------------------------------- /klogr_slog_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package klog_test 21 | 22 | import ( 23 | "errors" 24 | "flag" 25 | "fmt" 26 | "log/slog" 27 | "os" 28 | "time" 29 | 30 | "github.com/go-logr/logr" 31 | 32 | "k8s.io/klog/v2" 33 | internal "k8s.io/klog/v2/internal/buffer" 34 | ) 35 | 36 | var _ slog.LogValuer = coordinates{} 37 | 38 | type coordinates struct { 39 | x, y int 40 | } 41 | 42 | func (c coordinates) LogValue() slog.Value { 43 | return slog.GroupValue(slog.Attr{Key: "X", Value: slog.IntValue(c.x)}, slog.Attr{Key: "Y", Value: slog.IntValue(c.y)}) 44 | } 45 | 46 | func ExampleBackground_Slog() { 47 | // Temporarily reconfigure for output to stdout, with -v=4. 48 | state := klog.CaptureState() 49 | defer state.Restore() 50 | var fs flag.FlagSet 51 | klog.InitFlags(&fs) 52 | if err := fs.Set("logtostderr", "false"); err != nil { 53 | fmt.Println(err) 54 | } 55 | if err := fs.Set("alsologtostderr", "false"); err != nil { 56 | fmt.Println(err) 57 | } 58 | if err := fs.Set("v", "4"); err != nil { 59 | fmt.Println(err) 60 | } 61 | if err := fs.Set("one_output", "true"); err != nil { 62 | fmt.Println(err) 63 | } 64 | if err := fs.Set("skip_headers", "false"); err != nil { 65 | fmt.Println(err) 66 | } 67 | klog.SetOutput(os.Stdout) 68 | 69 | // To get consistent output for each run. 70 | ts, _ := time.Parse(time.RFC3339, "2000-12-24T12:30:40Z") 71 | internal.Time = &ts 72 | internal.Pid = 123 73 | 74 | logrLogger := klog.Background() 75 | slogHandler := logr.ToSlogHandler(logrLogger) 76 | slogLogger := slog.New(slogHandler) 77 | 78 | // Note that -vmodule does not work when using the slog API because 79 | // stack unwinding during the Enabled check leads to the wrong source 80 | // code. 81 | slogLogger.Debug("A debug message") 82 | slogLogger.Log(nil, slog.LevelDebug-1, "A debug message with even lower level, not printed.") 83 | slogLogger.Info("An info message") 84 | slogLogger.Warn("A warning") 85 | slogLogger.Error("An error", "err", errors.New("fake error")) 86 | 87 | // The slog API supports grouping, in contrast to the logr API. 88 | slogLogger.WithGroup("top").With("int", 42, slog.Group("variables", "a", 1, "b", 2)).Info("Grouping", 89 | "sub", slog.GroupValue( 90 | slog.Attr{Key: "str", Value: slog.StringValue("abc")}, 91 | slog.Attr{Key: "bool", Value: slog.BoolValue(true)}, 92 | slog.Attr{Key: "bottom", Value: slog.GroupValue(slog.Attr{Key: "coordinates", Value: slog.AnyValue(coordinates{x: -1, y: -2})})}, 93 | ), 94 | "duration", slog.DurationValue(time.Second), 95 | slog.Float64("pi", 3.12), 96 | "e", 2.71, 97 | "moreCoordinates", coordinates{x: 100, y: 200}, 98 | ) 99 | 100 | // slog special values are also supported when passed through the logr API. 101 | // This works with the textlogger, but might not work with other implementations 102 | // and thus isn't portable. Passing attributes (= key and value in a single 103 | // parameter) is not supported. 104 | logrLogger.Info("slog values", 105 | "variables", slog.GroupValue(slog.Int("a", 1), slog.Int("b", 2)), 106 | "duration", slog.DurationValue(time.Second), 107 | "coordinates", coordinates{x: 100, y: 200}, 108 | ) 109 | 110 | // Output: 111 | // I1224 12:30:40.000000 123 klogr_slog_test.go:81] "A debug message" 112 | // I1224 12:30:40.000000 123 klogr_slog_test.go:83] "An info message" 113 | // W1224 12:30:40.000000 123 klogr_slog_test.go:84] "A warning" 114 | // E1224 12:30:40.000000 123 klogr_slog_test.go:85] "An error" err="fake error" 115 | // I1224 12:30:40.000000 123 klogr_slog_test.go:88] "Grouping" top.sub={"str":"abc","bool":true,"bottom":{"coordinates":{"X":-1,"Y":-2}}} top.duration="1s" top.pi=3.12 top.e=2.71 top.moreCoordinates={"X":100,"Y":200} 116 | // I1224 12:30:40.000000 123 klogr_slog_test.go:104] "slog values" variables={"a":1,"b":2} duration="1s" coordinates={"X":100,"Y":200} 117 | } 118 | -------------------------------------------------------------------------------- /klogr_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klog_test 18 | 19 | import ( 20 | "bytes" 21 | "flag" 22 | "regexp" 23 | "testing" 24 | 25 | "k8s.io/klog/v2" 26 | ) 27 | 28 | func TestVerbosity(t *testing.T) { 29 | state := klog.CaptureState() 30 | defer state.Restore() 31 | 32 | var fs flag.FlagSet 33 | klog.InitFlags(&fs) 34 | if err := fs.Set("v", "5"); err != nil { 35 | t.Fatalf("unexpected error: %v", err) 36 | } 37 | if err := fs.Set("vmodule", "klogr_helper_test=10"); err != nil { 38 | t.Fatalf("unexpected error: %v", err) 39 | } 40 | if err := fs.Set("logtostderr", "false"); err != nil { 41 | t.Fatalf("unexpected error: %v", err) 42 | } 43 | var buffer bytes.Buffer 44 | klog.SetOutput(&buffer) 45 | logger := klog.Background() 46 | 47 | // -v=5 is in effect here. 48 | logger.V(6).Info("v6 not visible from klogr_test.go") 49 | if logger.V(6).Enabled() { 50 | t.Error("V(6).Enabled() in klogr_test.go should have returned false.") 51 | } 52 | logger.V(5).Info("v5 visible from klogr_test.go") 53 | if !logger.V(5).Enabled() { 54 | t.Error("V(5).Enabled() in klogr_test.go should have returned true.") 55 | } 56 | 57 | // Now test with -v=5 -vmodule=klogr_helper_test=10. 58 | testVerbosity(t, logger) 59 | 60 | klog.Flush() 61 | expected := `^.*v5 visible from klogr_test.go.* 62 | .*v10 visible from klogr_helper_test.go.* 63 | ` 64 | if !regexp.MustCompile(expected).Match(buffer.Bytes()) { 65 | t.Errorf("Output did not match regular expression.\nOutput:\n%s\n\nRegular expression:\n%s\n", buffer.String(), expected) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ktesting/contextual_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | Copyright 2020 Intel Corporation. 4 | 5 | SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | package ktesting_test 9 | 10 | import ( 11 | "context" 12 | "testing" 13 | 14 | "k8s.io/klog/v2" 15 | "k8s.io/klog/v2/ktesting" 16 | ) 17 | 18 | func TestContextual(t *testing.T) { 19 | var buffer ktesting.BufferTL 20 | logger, ctx := ktesting.NewTestContext(&buffer) 21 | 22 | doSomething(ctx) 23 | 24 | // When contextual logging is disabled, the output goes to klog 25 | // instead of the testing logger. 26 | state := klog.CaptureState() 27 | defer state.Restore() 28 | klog.EnableContextualLogging(false) 29 | doSomething(ctx) 30 | 31 | testingLogger, ok := logger.GetSink().(ktesting.Underlier) 32 | if !ok { 33 | t.Fatal("Should have had a ktesting LogSink!?") 34 | } 35 | 36 | actual := testingLogger.GetBuffer().String() 37 | if actual != "" { 38 | t.Errorf("testinglogger should not have buffered, got:\n%s", actual) 39 | } 40 | 41 | actual = buffer.String() 42 | actual = headerRe.ReplaceAllString(actual, "${1}xxx] ") 43 | expected := `Ixxx] hello world 44 | Ixxx] foo: hello also from me 45 | ` 46 | if actual != expected { 47 | t.Errorf("mismatch in captured output, expected:\n%s\ngot:\n%s\n", expected, actual) 48 | } 49 | } 50 | 51 | func doSomething(ctx context.Context) { 52 | logger := klog.FromContext(ctx) 53 | logger.Info("hello world") 54 | 55 | logger = logger.WithName("foo") 56 | ctx = klog.NewContext(ctx, logger) 57 | doSomeMore(ctx) 58 | } 59 | 60 | func doSomeMore(ctx context.Context) { 61 | logger := klog.FromContext(ctx) 62 | logger.Info("hello also from me") 63 | } 64 | -------------------------------------------------------------------------------- /ktesting/example/example_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | package example 8 | 9 | import ( 10 | "fmt" 11 | "testing" 12 | 13 | "k8s.io/klog/v2" 14 | "k8s.io/klog/v2/internal/test" 15 | "k8s.io/klog/v2/ktesting" 16 | _ "k8s.io/klog/v2/ktesting/init" // add command line flags 17 | ) 18 | 19 | func TestKlogr(t *testing.T) { 20 | logger, _ := ktesting.NewTestContext(t) 21 | exampleOutput(logger) 22 | } 23 | 24 | type pair struct { 25 | a, b int 26 | } 27 | 28 | func (p pair) String() string { 29 | return fmt.Sprintf("(%d, %d)", p.a, p.b) 30 | } 31 | 32 | var _ fmt.Stringer = pair{} 33 | 34 | type err struct { 35 | msg string 36 | } 37 | 38 | func (e err) Error() string { 39 | return "failed: " + e.msg 40 | } 41 | 42 | var _ error = err{} 43 | 44 | func exampleOutput(logger klog.Logger) { 45 | logger.Info("hello world") 46 | logger.Error(err{msg: "some error"}, "failed") 47 | logger.V(1).Info("verbosity 1") 48 | logger.WithName("main").WithName("helper").Info("with prefix") 49 | obj := test.KMetadataMock{Name: "joe", NS: "kube-system"} 50 | logger.Info("key/value pairs", 51 | "int", 1, 52 | "float", 2.0, 53 | "pair", pair{a: 1, b: 2}, 54 | "raw", obj, 55 | "kobj", klog.KObj(obj), 56 | ) 57 | logger.V(4).Info("info message level 4") 58 | logger.V(5).Info("info message level 5") 59 | logger.V(6).Info("info message level 6") 60 | } 61 | -------------------------------------------------------------------------------- /ktesting/example_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package ktesting_test 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "time" 23 | 24 | "k8s.io/klog/v2/ktesting" 25 | ) 26 | 27 | func ExampleUnderlier() { 28 | logger := ktesting.NewLogger(ktesting.NopTL{}, 29 | ktesting.NewConfig( 30 | ktesting.Verbosity(4), 31 | ktesting.BufferLogs(true), 32 | ktesting.AnyToString(func(value interface{}) string { 33 | return fmt.Sprintf("### %+v ###", value) 34 | }), 35 | ), 36 | ) 37 | 38 | logger.Error(errors.New("failure"), "I failed", "what", "something", "data", struct{ Field int }{Field: 1}) 39 | logger.WithValues("request", 42).WithValues("anotherValue", "fish").Info("hello world") 40 | logger.WithValues("request", 42, "anotherValue", "fish").Info("hello world 2", "yetAnotherValue", "thanks") 41 | logger.WithName("example").Info("with name") 42 | logger.V(4).Info("higher verbosity") 43 | logger.V(5).Info("Not captured because of ktesting.Verbosity(4) above. Normally it would be captured because default verbosity is 5.") 44 | 45 | testingLogger, ok := logger.GetSink().(ktesting.Underlier) 46 | if !ok { 47 | panic("Should have had a ktesting LogSink!?") 48 | } 49 | 50 | t := testingLogger.GetUnderlying() 51 | t.Log("This goes to /dev/null...") 52 | 53 | buffer := testingLogger.GetBuffer() 54 | fmt.Printf("%s\n", buffer.String()) 55 | 56 | log := buffer.Data() 57 | for i, entry := range log { 58 | if i > 0 && 59 | entry.Timestamp.Sub(log[i-1].Timestamp).Nanoseconds() < 0 { 60 | fmt.Printf("Unexpected timestamp order: #%d %s > #%d %s", i-1, log[i-1].Timestamp, i, entry.Timestamp) 61 | } 62 | // Strip varying time stamp before dumping the struct. 63 | entry.Timestamp = time.Time{} 64 | fmt.Printf("log entry #%d: %+v\n", i, entry) 65 | } 66 | 67 | // Output: 68 | // ERROR I failed err="failure" what="something" data=### {Field:1} ### 69 | // INFO hello world request=### 42 ### anotherValue="fish" 70 | // INFO hello world 2 request=### 42 ### anotherValue="fish" yetAnotherValue="thanks" 71 | // INFO example: with name 72 | // INFO higher verbosity 73 | // 74 | // log entry #0: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:ERROR Prefix: Message:I failed Verbosity:0 Err:failure WithKVList:[] ParameterKVList:[what something data {Field:1}]} 75 | // log entry #1: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:hello world Verbosity:0 Err: WithKVList:[request 42 anotherValue fish] ParameterKVList:[]} 76 | // log entry #2: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:hello world 2 Verbosity:0 Err: WithKVList:[request 42 anotherValue fish] ParameterKVList:[yetAnotherValue thanks]} 77 | // log entry #3: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix:example Message:with name Verbosity:0 Err: WithKVList:[] ParameterKVList:[]} 78 | // log entry #4: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:higher verbosity Verbosity:4 Err: WithKVList:[] ParameterKVList:[]} 79 | } 80 | 81 | func ExampleNewLogger() { 82 | var buffer ktesting.BufferTL 83 | logger := ktesting.NewLogger(&buffer, ktesting.NewConfig()) 84 | 85 | logger.Error(errors.New("failure"), "I failed", "what", "something", "data", struct{ Field int }{Field: 1}) 86 | logger.V(5).Info("Logged at level 5.") 87 | logger.V(6).Info("Not logged at level 6.") 88 | 89 | testingLogger, ok := logger.GetSink().(ktesting.Underlier) 90 | if !ok { 91 | panic("Should have had a ktesting LogSink!?") 92 | } 93 | fmt.Printf(">> %s <<\n", testingLogger.GetBuffer().String()) // Should be empty. 94 | fmt.Print(headerRe.ReplaceAllString(buffer.String(), "${1}...] ")) // Should not be empty. 95 | 96 | // Output: 97 | // >> << 98 | // E...] I failed err="failure" what="something" data={"Field":1} 99 | // I...] Logged at level 5. 100 | } 101 | 102 | func ExampleConfig_Verbosity() { 103 | var buffer ktesting.BufferTL 104 | config := ktesting.NewConfig(ktesting.Verbosity(1)) 105 | logger := ktesting.NewLogger(&buffer, config) 106 | 107 | logger.Info("initial verbosity", "v", config.Verbosity().String()) 108 | logger.V(2).Info("now you don't see me") 109 | if err := config.Verbosity().Set("2"); err != nil { 110 | logger.Error(err, "setting verbosity to 2") 111 | } 112 | logger.V(2).Info("now you see me") 113 | if err := config.Verbosity().Set("1"); err != nil { 114 | logger.Error(err, "setting verbosity to 1") 115 | } 116 | logger.V(2).Info("now I'm gone again") 117 | 118 | fmt.Print(headerRe.ReplaceAllString(buffer.String(), "${1}...] ")) 119 | 120 | // Output: 121 | // I...] initial verbosity v="1" 122 | // I...] now you see me 123 | } 124 | -------------------------------------------------------------------------------- /ktesting/init/init.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package init registers the command line flags for k8s.io/klogr/testing in 18 | // the flag.CommandLine. This is done during initialization, so merely 19 | // importing it is enough. 20 | package init 21 | 22 | import ( 23 | "flag" 24 | 25 | "k8s.io/klog/v2/ktesting" 26 | ) 27 | 28 | func init() { 29 | ktesting.DefaultConfig.AddFlags(flag.CommandLine) 30 | } 31 | -------------------------------------------------------------------------------- /ktesting/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package ktesting 18 | 19 | import ( 20 | "flag" 21 | "strconv" 22 | 23 | "k8s.io/klog/v2/internal/serialize" 24 | "k8s.io/klog/v2/internal/verbosity" 25 | ) 26 | 27 | // Config influences logging in a test logger. To make this configurable via 28 | // command line flags, instantiate this once per program and use AddFlags to 29 | // bind command line flags to the instance before passing it to NewTestContext. 30 | // 31 | // Must be constructed with NewConfig. 32 | type Config struct { 33 | vstate *verbosity.VState 34 | co configOptions 35 | } 36 | 37 | // Verbosity returns a value instance that can be used to query (via String) or 38 | // modify (via Set) the verbosity threshold. This is thread-safe and can be 39 | // done at runtime. 40 | func (c *Config) Verbosity() flag.Value { 41 | return c.vstate.V() 42 | } 43 | 44 | // VModule returns a value instance that can be used to query (via String) or 45 | // modify (via Set) the vmodule settings. This is thread-safe and can be done 46 | // at runtime. 47 | func (c *Config) VModule() flag.Value { 48 | return c.vstate.VModule() 49 | } 50 | 51 | // ConfigOption implements functional parameters for NewConfig. 52 | type ConfigOption func(co *configOptions) 53 | 54 | type configOptions struct { 55 | anyToString serialize.AnyToStringFunc 56 | verbosityFlagName string 57 | vmoduleFlagName string 58 | verbosityDefault int 59 | bufferLogs bool 60 | } 61 | 62 | // AnyToString overrides the default formatter for values that are not 63 | // supported directly by klog. The default is `fmt.Sprintf("%+v")`. 64 | // The formatter must not panic. 65 | func AnyToString(anyToString func(value interface{}) string) ConfigOption { 66 | return func(co *configOptions) { 67 | co.anyToString = anyToString 68 | } 69 | } 70 | 71 | // VerbosityFlagName overrides the default -testing.v for the verbosity level. 72 | func VerbosityFlagName(name string) ConfigOption { 73 | return func(co *configOptions) { 74 | co.verbosityFlagName = name 75 | } 76 | } 77 | 78 | // VModulFlagName overrides the default -testing.vmodule for the per-module 79 | // verbosity levels. 80 | func VModuleFlagName(name string) ConfigOption { 81 | return func(co *configOptions) { 82 | co.vmoduleFlagName = name 83 | } 84 | } 85 | 86 | // Verbosity overrides the default verbosity level of 5. That default is higher 87 | // than in klog itself because it enables logging entries for "the steps 88 | // leading up to errors and warnings" and "troubleshooting" (see 89 | // https://github.com/kubernetes/community/blob/9406b4352fe2d5810cb21cc3cb059ce5886de157/contributors/devel/sig-instrumentation/logging.md#logging-conventions), 90 | // which is useful when debugging a failed test. `go test` only shows the log 91 | // output for failed tests. To see all output, use `go test -v`. 92 | func Verbosity(level int) ConfigOption { 93 | return func(co *configOptions) { 94 | co.verbosityDefault = level 95 | } 96 | } 97 | 98 | // BufferLogs controls whether log entries are captured in memory in addition 99 | // to being printed. Off by default. Unit tests that want to verify that 100 | // log entries are emitted as expected can turn this on and then retrieve 101 | // the captured log through the Underlier LogSink interface. 102 | func BufferLogs(enabled bool) ConfigOption { 103 | return func(co *configOptions) { 104 | co.bufferLogs = enabled 105 | } 106 | } 107 | 108 | // NewConfig returns a configuration with recommended defaults and optional 109 | // modifications. Command line flags are not bound to any FlagSet yet. 110 | func NewConfig(opts ...ConfigOption) *Config { 111 | c := &Config{ 112 | co: configOptions{ 113 | verbosityFlagName: "testing.v", 114 | vmoduleFlagName: "testing.vmodule", 115 | verbosityDefault: 5, 116 | }, 117 | } 118 | for _, opt := range opts { 119 | opt(&c.co) 120 | } 121 | 122 | c.vstate = verbosity.New() 123 | // Cannot fail for this input. 124 | _ = c.vstate.V().Set(strconv.FormatInt(int64(c.co.verbosityDefault), 10)) 125 | return c 126 | } 127 | 128 | // AddFlags registers the command line flags that control the configuration. 129 | func (c *Config) AddFlags(fs *flag.FlagSet) { 130 | fs.Var(c.vstate.V(), c.co.verbosityFlagName, "number for the log level verbosity of the testing logger") 131 | fs.Var(c.vstate.VModule(), c.co.vmoduleFlagName, "comma-separated list of pattern=N log level settings for files matching the patterns") 132 | } 133 | -------------------------------------------------------------------------------- /ktesting/setup.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package ktesting 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/go-logr/logr" 23 | ) 24 | 25 | // DefaultConfig is the global default logging configuration for a unit 26 | // test. It is used by NewTestContext and k8s.io/klogr/testing/init. 27 | var DefaultConfig = NewConfig() 28 | 29 | // NewTestContext returns a logger and context for use in a unit test case or 30 | // benchmark. The tl parameter can be a testing.T or testing.B pointer that 31 | // will receive all log output. Importing k8s.io/klogr/testing/init will add 32 | // command line flags that modify the configuration of that log output. 33 | func NewTestContext(tl TL) (logr.Logger, context.Context) { 34 | logger := NewLogger(tl, DefaultConfig) 35 | ctx := logr.NewContext(context.Background(), logger) 36 | return logger, ctx 37 | 38 | } 39 | -------------------------------------------------------------------------------- /output_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package klog_test 18 | 19 | import ( 20 | "io" 21 | "testing" 22 | 23 | "github.com/go-logr/logr" 24 | 25 | "k8s.io/klog/v2" 26 | "k8s.io/klog/v2/test" 27 | ) 28 | 29 | // klogConfig tests klog output without a logger. 30 | var klogConfig = test.OutputConfig{} 31 | 32 | func TestKlogOutput(t *testing.T) { 33 | test.InitKlog(t) 34 | test.Output(t, klogConfig) 35 | } 36 | 37 | func BenchmarkKlogOutput(b *testing.B) { 38 | test.InitKlog(b) 39 | test.Benchmark(b, klogConfig) 40 | } 41 | 42 | // klogKlogrConfig tests klogr output via klog, using the klog/v2 klogr. 43 | var klogKLogrConfig = test.OutputConfig{ 44 | NewLogger: func(_ io.Writer, _ int, _ string) logr.Logger { 45 | return klog.NewKlogr() 46 | }, 47 | } 48 | 49 | func TestKlogrOutput(t *testing.T) { 50 | test.InitKlog(t) 51 | test.Output(t, klogKLogrConfig) 52 | } 53 | -------------------------------------------------------------------------------- /safeptr.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package klog 21 | 22 | // SafePtr is a function that takes a pointer of any type (T) as an argument. 23 | // If the provided pointer is not nil, it returns the same pointer. If it is nil, it returns nil instead. 24 | // 25 | // This function is particularly useful to prevent nil pointer dereferencing when: 26 | // 27 | // - The type implements interfaces that are called by the logger, such as `fmt.Stringer`. 28 | // - And these interface implementations do not perform nil checks themselves. 29 | func SafePtr[T any](p *T) any { 30 | if p == nil { 31 | return nil 32 | } 33 | return p 34 | } 35 | -------------------------------------------------------------------------------- /safeptr_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package klog_test 21 | 22 | import ( 23 | "testing" 24 | 25 | "k8s.io/klog/v2" 26 | ) 27 | 28 | func TestSafePtr(t *testing.T) { 29 | // Test with nil pointer 30 | var stringPtr *string 31 | if result := klog.SafePtr(stringPtr); result != nil { 32 | t.Errorf("Expected nil, got %p", result) 33 | } 34 | 35 | // Test with non-nil pointer 36 | expected := "foo" 37 | stringPtr = &expected 38 | if result := klog.SafePtr(stringPtr); result != stringPtr { 39 | t.Errorf("Expected %v, got %v", stringPtr, result) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/output_helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package test 18 | 19 | import ( 20 | "github.com/go-logr/logr" 21 | 22 | "k8s.io/klog/v2" 23 | ) 24 | 25 | func loggerHelper(logger logr.Logger, msg string, kv []interface{}) { 26 | logger = logger.WithCallDepth(1) 27 | logger.Info(msg, kv...) 28 | } 29 | 30 | func klogHelper(level klog.Level, msg string, kv []interface{}) { 31 | klog.V(level).InfoSDepth(1, msg, kv...) 32 | } 33 | -------------------------------------------------------------------------------- /textlogger/example_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package textlogger_test 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "regexp" 23 | 24 | "k8s.io/klog/v2/textlogger" 25 | ) 26 | 27 | var headerRe = regexp.MustCompile(`([IE])[[:digit:]]{4} [[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\.[[:digit:]]{6}[[:space:]]+[[:digit:]]+ example_test.go:[[:digit:]]+\] `) 28 | 29 | func ExampleConfig_Verbosity() { 30 | var buffer bytes.Buffer 31 | config := textlogger.NewConfig(textlogger.Verbosity(1), textlogger.Output(&buffer)) 32 | logger := textlogger.NewLogger(config) 33 | 34 | logger.Info("initial verbosity", "v", config.Verbosity().String()) 35 | logger.V(2).Info("now you don't see me") 36 | if err := config.Verbosity().Set("2"); err != nil { 37 | logger.Error(err, "setting verbosity to 2") 38 | } 39 | logger.V(2).Info("now you see me") 40 | if err := config.Verbosity().Set("1"); err != nil { 41 | logger.Error(err, "setting verbosity to 1") 42 | } 43 | logger.V(2).Info("now I'm gone again") 44 | 45 | fmt.Print(headerRe.ReplaceAllString(buffer.String(), "${1}...] ")) 46 | 47 | // Output: 48 | // I...] "initial verbosity" v="1" 49 | // I...] "now you see me" 50 | } 51 | -------------------------------------------------------------------------------- /textlogger/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package textlogger 18 | 19 | import ( 20 | "flag" 21 | "io" 22 | "os" 23 | "strconv" 24 | "time" 25 | 26 | "k8s.io/klog/v2/internal/verbosity" 27 | ) 28 | 29 | // Config influences logging in a text logger. To make this configurable via 30 | // command line flags, instantiate this once per program and use AddFlags to 31 | // bind command line flags to the instance before passing it to NewTestContext. 32 | // 33 | // Must be constructed with NewConfig. 34 | type Config struct { 35 | vstate *verbosity.VState 36 | co configOptions 37 | } 38 | 39 | // Verbosity returns a value instance that can be used to query (via String) or 40 | // modify (via Set) the verbosity threshold. This is thread-safe and can be 41 | // done at runtime. 42 | func (c *Config) Verbosity() flag.Value { 43 | return c.vstate.V() 44 | } 45 | 46 | // VModule returns a value instance that can be used to query (via String) or 47 | // modify (via Set) the vmodule settings. This is thread-safe and can be done 48 | // at runtime. 49 | func (c *Config) VModule() flag.Value { 50 | return c.vstate.VModule() 51 | } 52 | 53 | // ConfigOption implements functional parameters for NewConfig. 54 | type ConfigOption func(co *configOptions) 55 | 56 | type configOptions struct { 57 | verbosityFlagName string 58 | vmoduleFlagName string 59 | verbosityDefault int 60 | fixedTime *time.Time 61 | unwind func(int) (string, int) 62 | output io.Writer 63 | } 64 | 65 | // VerbosityFlagName overrides the default -v for the verbosity level. 66 | func VerbosityFlagName(name string) ConfigOption { 67 | return func(co *configOptions) { 68 | 69 | co.verbosityFlagName = name 70 | } 71 | } 72 | 73 | // VModulFlagName overrides the default -vmodule for the per-module 74 | // verbosity levels. 75 | func VModuleFlagName(name string) ConfigOption { 76 | return func(co *configOptions) { 77 | co.vmoduleFlagName = name 78 | } 79 | } 80 | 81 | // Verbosity overrides the default verbosity level of 0. 82 | // See https://github.com/kubernetes/community/blob/9406b4352fe2d5810cb21cc3cb059ce5886de157/contributors/devel/sig-instrumentation/logging.md#logging-conventions 83 | // for log level conventions in Kubernetes. 84 | func Verbosity(level int) ConfigOption { 85 | return func(co *configOptions) { 86 | co.verbosityDefault = level 87 | } 88 | } 89 | 90 | // Output overrides stderr as the output stream. 91 | func Output(output io.Writer) ConfigOption { 92 | return func(co *configOptions) { 93 | co.output = output 94 | } 95 | } 96 | 97 | // FixedTime overrides the actual time with a fixed time. Useful only for testing. 98 | // 99 | // # Experimental 100 | // 101 | // Notice: This function is EXPERIMENTAL and may be changed or removed in a 102 | // later release. 103 | func FixedTime(ts time.Time) ConfigOption { 104 | return func(co *configOptions) { 105 | co.fixedTime = &ts 106 | } 107 | } 108 | 109 | // Backtrace overrides the default mechanism for determining the call site. 110 | // The callback is invoked with the number of function calls between itself 111 | // and the call site. It must return the file name and line number. An empty 112 | // file name indicates that the information is unknown. 113 | // 114 | // # Experimental 115 | // 116 | // Notice: This function is EXPERIMENTAL and may be changed or removed in a 117 | // later release. 118 | func Backtrace(unwind func(skip int) (filename string, line int)) ConfigOption { 119 | return func(co *configOptions) { 120 | co.unwind = unwind 121 | } 122 | } 123 | 124 | // NewConfig returns a configuration with recommended defaults and optional 125 | // modifications. Command line flags are not bound to any FlagSet yet. 126 | func NewConfig(opts ...ConfigOption) *Config { 127 | c := &Config{ 128 | vstate: verbosity.New(), 129 | co: configOptions{ 130 | verbosityFlagName: "v", 131 | vmoduleFlagName: "vmodule", 132 | verbosityDefault: 0, 133 | unwind: runtimeBacktrace, 134 | output: os.Stderr, 135 | }, 136 | } 137 | for _, opt := range opts { 138 | opt(&c.co) 139 | } 140 | 141 | // Cannot fail for this input. 142 | _ = c.Verbosity().Set(strconv.FormatInt(int64(c.co.verbosityDefault), 10)) 143 | return c 144 | } 145 | 146 | // AddFlags registers the command line flags that control the configuration. 147 | // 148 | // The default flag names are the same as in klog, so unless those defaults 149 | // are changed, either klog.InitFlags or Config.AddFlags can be used for the 150 | // same flag set, but not both. 151 | func (c *Config) AddFlags(fs *flag.FlagSet) { 152 | fs.Var(c.Verbosity(), c.co.verbosityFlagName, "number for the log level verbosity of the testing logger") 153 | fs.Var(c.VModule(), c.co.vmoduleFlagName, "comma-separated list of pattern=N log level settings for files matching the patterns") 154 | } 155 | -------------------------------------------------------------------------------- /textlogger/output_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package textlogger_test 18 | 19 | import ( 20 | "io" 21 | "testing" 22 | 23 | "github.com/go-logr/logr" 24 | 25 | "k8s.io/klog/v2/test" 26 | "k8s.io/klog/v2/textlogger" 27 | ) 28 | 29 | // These test cover the textlogger, directly and as backend. 30 | func newLogger(out io.Writer, v int, vmodule string) logr.Logger { 31 | config := textlogger.NewConfig( 32 | textlogger.Verbosity(v), 33 | textlogger.Output(out), 34 | ) 35 | if err := config.VModule().Set(vmodule); err != nil { 36 | panic(err) 37 | } 38 | return textlogger.NewLogger(config) 39 | } 40 | 41 | var ( 42 | directConfig = test.OutputConfig{NewLogger: newLogger, SupportsVModule: true} 43 | indirectConfig = test.OutputConfig{NewLogger: newLogger, AsBackend: true} 44 | ) 45 | 46 | func TestTextloggerOutput(t *testing.T) { 47 | test.InitKlog(t) 48 | t.Run("direct", func(t *testing.T) { 49 | test.Output(t, directConfig) 50 | }) 51 | t.Run("klog-backend", func(t *testing.T) { 52 | test.Output(t, indirectConfig) 53 | }) 54 | } 55 | 56 | func BenchmarkTextloggerOutput(b *testing.B) { 57 | test.InitKlog(b) 58 | test.Benchmark(b, directConfig) 59 | } 60 | -------------------------------------------------------------------------------- /textlogger/textlogger.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | Copyright 2020 Intel Corporation. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | // Package textlogger contains an implementation of the logr interface which is 19 | // producing the exact same output as klog. It does not route output through 20 | // klog (i.e. ignores [k8s.io/klog/v2.InitFlags]). Instead, all settings must be 21 | // configured through its own [NewConfig] and [Config.AddFlags]. 22 | package textlogger 23 | 24 | import ( 25 | "runtime" 26 | "strconv" 27 | "strings" 28 | "time" 29 | 30 | "github.com/go-logr/logr" 31 | 32 | "k8s.io/klog/v2/internal/buffer" 33 | "k8s.io/klog/v2/internal/serialize" 34 | "k8s.io/klog/v2/internal/severity" 35 | "k8s.io/klog/v2/internal/verbosity" 36 | ) 37 | 38 | var ( 39 | // TimeNow is used to retrieve the current time. May be changed for testing. 40 | TimeNow = time.Now 41 | ) 42 | 43 | const ( 44 | // nameKey is used to log the `WithName` values as an additional attribute. 45 | nameKey = "logger" 46 | ) 47 | 48 | // NewLogger constructs a new logger. 49 | // 50 | // Verbosity can be modified at any time through the Config.V and 51 | // Config.VModule API. 52 | func NewLogger(c *Config) logr.Logger { 53 | return logr.New(&tlogger{ 54 | values: nil, 55 | config: c, 56 | }) 57 | } 58 | 59 | type tlogger struct { 60 | callDepth int 61 | 62 | // hasPrefix is true if the first entry in values is the special 63 | // nameKey key/value. Such an entry gets added and later updated in 64 | // WithName. 65 | hasPrefix bool 66 | 67 | values []interface{} 68 | groups string 69 | config *Config 70 | } 71 | 72 | func (l *tlogger) Init(info logr.RuntimeInfo) { 73 | l.callDepth = info.CallDepth 74 | } 75 | 76 | func (l *tlogger) WithCallDepth(depth int) logr.LogSink { 77 | newLogger := *l 78 | newLogger.callDepth += depth 79 | return &newLogger 80 | } 81 | 82 | func (l *tlogger) Enabled(level int) bool { 83 | return l.config.vstate.Enabled(verbosity.Level(level), 1+l.callDepth) 84 | } 85 | 86 | func (l *tlogger) Info(_ int, msg string, kvList ...interface{}) { 87 | l.print(nil, severity.InfoLog, msg, kvList) 88 | } 89 | 90 | func (l *tlogger) Error(err error, msg string, kvList ...interface{}) { 91 | l.print(err, severity.ErrorLog, msg, kvList) 92 | } 93 | 94 | func (l *tlogger) print(err error, s severity.Severity, msg string, kvList []interface{}) { 95 | // Determine caller. 96 | // +1 for this frame, +1 for Info/Error. 97 | skip := l.callDepth + 2 98 | file, line := l.config.co.unwind(skip) 99 | if file == "" { 100 | file = "???" 101 | line = 1 102 | } else if slash := strings.LastIndex(file, "/"); slash >= 0 { 103 | file = file[slash+1:] 104 | } 105 | l.printWithInfos(file, line, time.Now(), err, s, msg, kvList) 106 | } 107 | 108 | func runtimeBacktrace(skip int) (string, int) { 109 | _, file, line, ok := runtime.Caller(skip + 1) 110 | if !ok { 111 | return "", 0 112 | } 113 | return file, line 114 | } 115 | 116 | func (l *tlogger) printWithInfos(file string, line int, now time.Time, err error, s severity.Severity, msg string, kvList []interface{}) { 117 | // The message is always quoted, even if it contains line breaks. 118 | // If developers want multi-line output, they should use a small, fixed 119 | // message and put the multi-line output into a value. 120 | qMsg := make([]byte, 0, 1024) 121 | qMsg = strconv.AppendQuote(qMsg, msg) 122 | 123 | // Only create a new buffer if we don't have one cached. 124 | b := buffer.GetBuffer() 125 | defer buffer.PutBuffer(b) 126 | 127 | // Format header. 128 | if l.config.co.fixedTime != nil { 129 | now = *l.config.co.fixedTime 130 | } 131 | b.FormatHeader(s, file, line, now) 132 | 133 | b.Write(qMsg) 134 | 135 | var errKV []interface{} 136 | if err != nil { 137 | errKV = []interface{}{"err", err} 138 | } 139 | serialize.FormatKVs(&b.Buffer, errKV, l.values, kvList) 140 | if b.Len() == 0 || b.Bytes()[b.Len()-1] != '\n' { 141 | b.WriteByte('\n') 142 | } 143 | _, _ = l.config.co.output.Write(b.Bytes()) 144 | } 145 | 146 | func (l *tlogger) WriteKlogBuffer(data []byte) { 147 | _, _ = l.config.co.output.Write(data) 148 | } 149 | 150 | // WithName returns a new logr.Logger with the specified name appended. klogr 151 | // uses '/' characters to separate name elements. Callers should not pass '/' 152 | // in the provided name string, but this library does not actually enforce that. 153 | func (l *tlogger) WithName(name string) logr.LogSink { 154 | clone := *l 155 | if l.hasPrefix { 156 | // Copy slice and modify value. No length checks and type 157 | // assertions are needed because hasPrefix is only true if the 158 | // first two elements exist and are key/value strings. 159 | v := make([]interface{}, 0, len(l.values)) 160 | v = append(v, l.values...) 161 | prefix, _ := v[1].(string) 162 | v[1] = prefix + "." + name 163 | clone.values = v 164 | } else { 165 | // Preprend new key/value pair. 166 | v := make([]interface{}, 0, 2+len(l.values)) 167 | v = append(v, nameKey, name) 168 | v = append(v, l.values...) 169 | clone.values = v 170 | clone.hasPrefix = true 171 | } 172 | return &clone 173 | } 174 | 175 | func (l *tlogger) WithValues(kvList ...interface{}) logr.LogSink { 176 | clone := *l 177 | clone.values = serialize.WithValues(l.values, kvList) 178 | return &clone 179 | } 180 | 181 | // KlogBufferWriter is implemented by the textlogger LogSink. 182 | type KlogBufferWriter interface { 183 | // WriteKlogBuffer takes a pre-formatted buffer prepared by klog and 184 | // writes it unchanged to the output stream. Can be used with 185 | // klog.WriteKlogBuffer when setting a logger through 186 | // klog.SetLoggerWithOptions. 187 | WriteKlogBuffer([]byte) 188 | } 189 | 190 | var _ logr.LogSink = &tlogger{} 191 | var _ logr.CallDepthLogSink = &tlogger{} 192 | var _ KlogBufferWriter = &tlogger{} 193 | -------------------------------------------------------------------------------- /textlogger/textlogger_slog.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package textlogger 21 | 22 | import ( 23 | "context" 24 | "log/slog" 25 | 26 | "github.com/go-logr/logr" 27 | 28 | "k8s.io/klog/v2/internal/serialize" 29 | "k8s.io/klog/v2/internal/sloghandler" 30 | ) 31 | 32 | func (l *tlogger) Handle(ctx context.Context, record slog.Record) error { 33 | return sloghandler.Handle(ctx, record, l.groups, l.printWithInfos) 34 | } 35 | 36 | func (l *tlogger) WithAttrs(attrs []slog.Attr) logr.SlogSink { 37 | clone := *l 38 | clone.values = serialize.WithValues(l.values, sloghandler.Attrs2KVList(l.groups, attrs)) 39 | return &clone 40 | } 41 | 42 | func (l *tlogger) WithGroup(name string) logr.SlogSink { 43 | clone := *l 44 | if clone.groups != "" { 45 | clone.groups += "." + name 46 | } else { 47 | clone.groups = name 48 | } 49 | return &clone 50 | } 51 | 52 | var _ logr.SlogSink = &tlogger{} 53 | -------------------------------------------------------------------------------- /textlogger/textlogger_slog_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | /* 5 | Copyright 2023 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package textlogger_test 21 | 22 | import ( 23 | "errors" 24 | "log/slog" 25 | "os" 26 | "time" 27 | 28 | "github.com/go-logr/logr/slogr" 29 | internal "k8s.io/klog/v2/internal/buffer" 30 | "k8s.io/klog/v2/textlogger" 31 | ) 32 | 33 | var _ slog.LogValuer = coordinates{} 34 | 35 | type coordinates struct { 36 | x, y int 37 | } 38 | 39 | func (c coordinates) LogValue() slog.Value { 40 | return slog.GroupValue(slog.Attr{Key: "X", Value: slog.IntValue(c.x)}, slog.Attr{Key: "Y", Value: slog.IntValue(c.y)}) 41 | } 42 | 43 | func ExampleNewLogger_Slog() { 44 | ts, _ := time.Parse(time.RFC3339, "2000-12-24T12:30:40Z") 45 | internal.Pid = 123 // To get consistent output for each run. 46 | config := textlogger.NewConfig( 47 | textlogger.FixedTime(ts), // To get consistent output for each run. 48 | textlogger.Verbosity(4), // Matches slog.LevelDebug. 49 | textlogger.Output(os.Stdout), 50 | ) 51 | logrLogger := textlogger.NewLogger(config) 52 | slogHandler := slogr.NewSlogHandler(logrLogger) 53 | slogLogger := slog.New(slogHandler) 54 | 55 | slogLogger.Debug("A debug message") 56 | slogLogger.Log(nil, slog.LevelDebug-1, "A debug message with even lower level, not printed.") 57 | slogLogger.Info("An info message") 58 | slogLogger.Warn("A warning") 59 | slogLogger.Error("An error", "err", errors.New("fake error")) 60 | 61 | // The slog API supports grouping, in contrast to the logr API. 62 | slogLogger.WithGroup("top").With("int", 42, slog.Group("variables", "a", 1, "b", 2)).Info("Grouping", 63 | "sub", slog.GroupValue( 64 | slog.Attr{Key: "str", Value: slog.StringValue("abc")}, 65 | slog.Attr{Key: "bool", Value: slog.BoolValue(true)}, 66 | slog.Attr{Key: "bottom", Value: slog.GroupValue(slog.Attr{Key: "coordinates", Value: slog.AnyValue(coordinates{x: -1, y: -2})})}, 67 | ), 68 | "duration", slog.DurationValue(time.Second), 69 | slog.Float64("pi", 3.12), 70 | "e", 2.71, 71 | "moreCoordinates", coordinates{x: 100, y: 200}, 72 | ) 73 | 74 | // slog special values are also supported when passed through the logr API. 75 | // This works with the textlogger, but might not work with other implementations 76 | // and thus isn't portable. Passing attributes (= key and value in a single 77 | // parameter) is not supported. 78 | logrLogger.Info("slog values", 79 | "variables", slog.GroupValue(slog.Int("a", 1), slog.Int("b", 2)), 80 | "duration", slog.DurationValue(time.Second), 81 | "coordinates", coordinates{x: 100, y: 200}, 82 | ) 83 | 84 | // Output: 85 | // I1224 12:30:40.000000 123 textlogger_slog_test.go:55] "A debug message" 86 | // I1224 12:30:40.000000 123 textlogger_slog_test.go:57] "An info message" 87 | // W1224 12:30:40.000000 123 textlogger_slog_test.go:58] "A warning" 88 | // E1224 12:30:40.000000 123 textlogger_slog_test.go:59] "An error" err="fake error" 89 | // I1224 12:30:40.000000 123 textlogger_slog_test.go:62] "Grouping" top.int=42 top.variables={"a":1,"b":2} top.sub={"str":"abc","bool":true,"bottom":{"coordinates":{"X":-1,"Y":-2}}} top.duration="1s" top.pi=3.12 top.e=2.71 top.moreCoordinates={"X":100,"Y":200} 90 | // I1224 12:30:40.000000 123 textlogger_slog_test.go:78] "slog values" variables={"a":1,"b":2} duration="1s" coordinates={"X":100,"Y":200} 91 | } 92 | -------------------------------------------------------------------------------- /textlogger/textlogger_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package textlogger_test 18 | 19 | import ( 20 | "errors" 21 | "os" 22 | "time" 23 | 24 | "github.com/go-logr/logr" 25 | "k8s.io/klog/v2" 26 | internal "k8s.io/klog/v2/internal/buffer" 27 | "k8s.io/klog/v2/textlogger" 28 | ) 29 | 30 | var _ logr.Marshaler = coordinatesMarshaler{} 31 | 32 | type coordinatesMarshaler struct { 33 | x, y int 34 | } 35 | 36 | func (c coordinatesMarshaler) MarshalLog() interface{} { 37 | return map[string]int{"X": c.x, "Y": c.y} 38 | } 39 | 40 | type variables struct { 41 | A, B int 42 | } 43 | 44 | func ExampleNewLogger() { 45 | ts, _ := time.Parse(time.RFC3339, "2000-12-24T12:30:40Z") 46 | internal.Pid = 123 // To get consistent output for each run. 47 | config := textlogger.NewConfig( 48 | textlogger.FixedTime(ts), // To get consistent output for each run. 49 | textlogger.Verbosity(4), // Matches Kubernetes "debug" level. 50 | textlogger.Output(os.Stdout), 51 | ) 52 | logger := textlogger.NewLogger(config) 53 | 54 | logger.V(4).Info("A debug message") 55 | logger.V(5).Info("A debug message with even lower level, not printed.") 56 | logger.Info("An info message") 57 | logger.Error(errors.New("fake error"), "An error") 58 | logger.WithValues("int", 42).Info("With values", 59 | "duration", time.Second, 60 | "float", 3.12, 61 | "coordinates", coordinatesMarshaler{x: 100, y: 200}, 62 | "variables", variables{A: 1, B: 2}, 63 | ) 64 | // The logr API supports skipping functions during stack unwinding, in contrast to slog. 65 | someHelper(logger, "hello world") 66 | 67 | // Output: 68 | // I1224 12:30:40.000000 123 textlogger_test.go:54] "A debug message" 69 | // I1224 12:30:40.000000 123 textlogger_test.go:56] "An info message" 70 | // E1224 12:30:40.000000 123 textlogger_test.go:57] "An error" err="fake error" 71 | // I1224 12:30:40.000000 123 textlogger_test.go:58] "With values" int=42 duration="1s" float=3.12 coordinates={"X":100,"Y":200} variables={"A":1,"B":2} 72 | // I1224 12:30:40.000000 123 textlogger_test.go:65] "hello world" 73 | } 74 | 75 | func someHelper(logger klog.Logger, msg string) { 76 | logger.WithCallDepth(1).Info(msg) 77 | } 78 | 79 | func ExampleBacktrace() { 80 | ts, _ := time.Parse(time.RFC3339, "2000-12-24T12:30:40Z") 81 | internal.Pid = 123 // To get consistent output for each run. 82 | backtraceCounter := 0 83 | config := textlogger.NewConfig( 84 | textlogger.FixedTime(ts), // To get consistent output for each run. 85 | textlogger.Backtrace(func(_ /* skip */ int) (filename string, line int) { 86 | backtraceCounter++ 87 | if backtraceCounter == 1 { 88 | // Simulate "missing information". 89 | return "", 0 90 | } 91 | return "fake.go", 42 92 | 93 | // A real implementation could use Ginkgo: 94 | // 95 | // import ginkgotypes "github.com/onsi/ginkgo/v2/types" 96 | // 97 | // location := ginkgotypes.NewCodeLocation(skip + 1) 98 | // return location.FileName, location.LineNumber 99 | }), 100 | textlogger.Output(os.Stdout), 101 | ) 102 | logger := textlogger.NewLogger(config) 103 | 104 | logger.Info("First message") 105 | logger.Info("Second message") 106 | 107 | // Output: 108 | // I1224 12:30:40.000000 123 ???:1] "First message" 109 | // I1224 12:30:40.000000 123 fake.go:42] "Second message" 110 | } 111 | --------------------------------------------------------------------------------