├── .github ├── dependabot.yml └── workflows │ ├── checks.yml │ └── release.yml ├── .golangci.yml ├── .slog.example.yml ├── LICENSE ├── Makefile ├── README.md ├── example └── example.go ├── example_test.go ├── go.mod ├── go.sum ├── main.go ├── sloggen.go ├── sloggen_test.go └── template.go.tmpl /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: checks 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | uses: go-simpler/.github/.github/workflows/test.yml@main 13 | lint: 14 | uses: go-simpler/.github/.github/workflows/lint.yml@main 15 | vuln: 16 | uses: go-simpler/.github/.github/workflows/vuln.yml@main 17 | generate: 18 | uses: go-simpler/.github/.github/workflows/generate.yml@main 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | type: string 10 | required: true 11 | description: The next semantic version to release 12 | 13 | jobs: 14 | release: 15 | uses: go-simpler/.github/.github/workflows/release.yml@main 16 | with: 17 | version: ${{ github.event_name == 'push' && github.ref_name || inputs.version }} 18 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | # enabled by default: 5 | - errcheck 6 | - gosimple 7 | - govet 8 | - ineffassign 9 | - staticcheck 10 | - typecheck 11 | - unused 12 | # disabled by default: 13 | - gocritic 14 | - gofumpt 15 | 16 | linters-settings: 17 | gocritic: 18 | enabled-tags: 19 | - diagnostic 20 | - style 21 | - performance 22 | - experimental 23 | - opinionated 24 | -------------------------------------------------------------------------------- /.slog.example.yml: -------------------------------------------------------------------------------- 1 | # the name for the generated package. 2 | # default: slogx 3 | pkg: example 4 | 5 | # the list of packages to import. 6 | # should only be filled in if non-basic types are used. 7 | # default: [] 8 | imports: 9 | - time 10 | 11 | # the list of levels to generate constants for. 12 | # format: 13 | # default: [] 14 | levels: 15 | - info: 0 16 | - alert: 12 17 | 18 | # the list of keys to generate constants for. 19 | # default: [] 20 | consts: 21 | - request_id 22 | 23 | # the list of attributes to generate constructors for. 24 | # format: 25 | # default: [] 26 | attrs: 27 | - user_id: int 28 | - created_at: time.Time 29 | - err: error 30 | 31 | # if present, a custom Logger type is generated with a method for each level. 32 | # if no levels are specified, the builtin slog levels are used. 33 | logger: 34 | # the API style for the Logger's methods. 35 | # possible values: [any, attr] 36 | # default: any 37 | api: attr 38 | # if true, the Logger's methods are generated with context.Context as the first parameter. 39 | # default: false 40 | ctx: true 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | .SUFFIXES: 3 | 4 | all: test lint 5 | 6 | test: 7 | go test -race -shuffle=on -cover ./... 8 | 9 | test/cover: 10 | go test -race -shuffle=on -coverprofile=coverage.out ./... 11 | go tool cover -html=coverage.out 12 | 13 | lint: 14 | golangci-lint run 15 | 16 | tidy: 17 | go mod tidy 18 | 19 | generate: 20 | go generate ./... 21 | 22 | # run `make pre-commit` once to install the hook. 23 | pre-commit: .git/hooks/pre-commit test lint tidy generate 24 | git diff --exit-code 25 | 26 | .git/hooks/pre-commit: 27 | echo "make pre-commit" > .git/hooks/pre-commit 28 | chmod +x .git/hooks/pre-commit 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sloggen 2 | 3 | [![checks](https://github.com/go-simpler/sloggen/actions/workflows/checks.yml/badge.svg)](https://github.com/go-simpler/sloggen/actions/workflows/checks.yml) 4 | [![pkg.go.dev](https://pkg.go.dev/badge/go-simpler.org/sloggen.svg)](https://pkg.go.dev/go-simpler.org/sloggen) 5 | [![goreportcard](https://goreportcard.com/badge/go-simpler.org/sloggen)](https://goreportcard.com/report/go-simpler.org/sloggen) 6 | [![codecov](https://codecov.io/gh/go-simpler/sloggen/branch/main/graph/badge.svg)](https://codecov.io/gh/go-simpler/sloggen) 7 | 8 | Generate domain-specific wrappers for `log/slog`. 9 | 10 | ## 📌 About 11 | 12 | When using `log/slog` in a production-grade project, it is useful to write helpers to prevent typos in the keys: 13 | 14 | ```go 15 | slog.Info("a user has logged in", "user_id", 42) 16 | slog.Info("a user has logged out", "user_ip", 42) // oops :( 17 | ``` 18 | 19 | Depending on your code style, these can be simple constants (if you prefer key-value pairs)... 20 | 21 | ```go 22 | const UserId = "user_id" 23 | ``` 24 | 25 | ...or custom `slog.Attr` constructors (if you're a safety/performance advocate): 26 | 27 | ```go 28 | func UserId(value int) slog.Attr { return slog.Int("user_id", value) } 29 | ``` 30 | 31 | `sloggen` generates such helpers for you, so you don't have to write them manually. 32 | 33 | --- 34 | 35 | The default `log/slog` levels cover most use cases, but at some point you may want to introduce custom levels that better suit your app. 36 | At first glance, this is as simple as defining a constant: 37 | 38 | ```go 39 | const LevelAlert = slog.Level(12) 40 | ``` 41 | 42 | However, custom levels are treated differently than the first-class citizens `Debug`/`Info`/`Warn`/`Error`: 43 | 44 | ```go 45 | slog.Log(nil, LevelAlert, "msg") // want "ALERT msg"; got "ERROR+4 msg" 46 | ``` 47 | 48 | `sloggen` solves this inconvenience by generating not only the levels themselves, but also the necessary helpers. 49 | 50 | Unfortunately, the only way to use such levels is the `Log` method, which is quite verbose. 51 | `sloggen` can generate a custom `Logger` type so that custom levels can be used just like the builtin ones: 52 | 53 | ```go 54 | // before: 55 | logger.Log(nil, LevelAlert, "msg", "key", "value") 56 | // after: 57 | logger.Alert("msg", "key", "value") 58 | ``` 59 | 60 | Additionally, there are options to choose the API style of the arguments (`...any` or `...slog.Attr`) and to add/remove `context.Context` as the first parameter. 61 | This allows you to adjust the logging API to your own code style without sacrificing convenience. 62 | 63 | > [!tip] 64 | > Various API rules for `log/slog` can be enforced by the [`sloglint`][1] linter. Give it a try too! 65 | 66 | ## 🚀 Features 67 | 68 | * Generate key constants and `slog.Attr` constructors 69 | * Generate custom levels with helpers for parsing/printing 70 | * Generate a custom `Logger` type with methods for custom levels 71 | * Codegen-based, so no runtime dependency introduced 72 | 73 | ## 📦 Install 74 | 75 | Add the following directive to any `.go` file and run `go generate ./...`. 76 | 77 | ```go 78 | //go:generate go run go-simpler.org/sloggen@ [flags] 79 | ``` 80 | 81 | Where `` is the version of `sloggen` itself (use `latest` for automatic updates) and `[flags]` is the list of [available options](#help). 82 | 83 | ## 📋 Usage 84 | 85 | There are two ways to provide options to `sloggen`: CLI flags and a `.yml` config file. 86 | The former works best for few options and requires only a single `//go:generate` directive. 87 | For many options it may be more convenient to use a config file, since `go generate` does not support multiline commands. 88 | The config file can also be reused between several (micro)services if they share the same domain. 89 | 90 | To get started, see the [`example_test.go`](example_test.go) file and the [`example`](example) directory. 91 | 92 | ### Key constants 93 | 94 | The `-c` flag (or the `consts` field) is used to generate a key constant. 95 | For example, `-c=used_id` results in: 96 | 97 | ```go 98 | const UserId = "user_id" 99 | ``` 100 | 101 | ### Attribute constructors 102 | 103 | The `-a` flag (or the `attrs` field) is used to generate a custom `slog.Attr` constructor. 104 | For example, `-a=used_id:int` results in: 105 | 106 | ```go 107 | func UserId(value int) slog.Attr { return slog.Int("user_id", value) } 108 | ``` 109 | 110 | ### Custom levels 111 | 112 | The `-l` flag (or the `levels` field) is used to generate a custom `slog.Level`. 113 | For example, `-l=alert:12` results in: 114 | 115 | ```go 116 | const LevelAlert = slog.Level(12) 117 | 118 | func ParseLevel(s string) (slog.Level, error) {...} 119 | func RenameLevels(_ []string, attr slog.Attr) slog.Attr {...} 120 | ``` 121 | 122 | The `ParseLevel` function should be used to parse the level from a string (e.g. from an environment variable): 123 | 124 | ```go 125 | level, err := slogx.ParseLevel("ALERT") 126 | ``` 127 | 128 | The `RenameLevels` function should be used as `slog.HandlerOptions.ReplaceAttr` to print custom level names correctly: 129 | 130 | ```go 131 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 132 | Level: level, 133 | ReplaceAttr: slogx.RenameLevels, 134 | })) 135 | ``` 136 | 137 | ### Custom Logger 138 | 139 | The `-logger` flag (or the `logger` field) is used to generate a custom `Logger` type with methods for custom levels. 140 | 141 | The `-api` flag (or the `logger.api` field) is used to choose the API style of the arguments: `any` for `...any` (key-value pairs) and `attr` for `...slog.Attr`. 142 | 143 | The `-ctx` flag (or the `logger.ctx` field) is used to add or remove `context.Context` as the first parameter. 144 | 145 | For example, `-l=alert:12 -logger -api=attr -ctx` results in: 146 | 147 | ```go 148 | type Logger struct{ handler slog.Handler } 149 | 150 | func New(h slog.Handler) *Logger { return &Logger{handler: h} } 151 | 152 | func (l *Logger) Alert(ctx context.Context, msg string, attrs ...slog.Attr) {...} 153 | ``` 154 | 155 | The generated `Logger` has all the utility methods of the original `slog.Logger`, including `Enabled()`, `With()` and `WithGroup()`. 156 | 157 | Since `Logger` is just a frontend, you can always fall back to `slog.Logger` (e.g. to pass it to a library) using the `Handler()` method: 158 | 159 | ```go 160 | slog.New(logger.Handler()) 161 | ``` 162 | 163 | ### Help 164 | 165 | ```shell 166 | Usage: sloggen [flags] 167 | 168 | Flags: 169 | -config read config from the file instead of flags 170 | -dir change the working directory before generating files 171 | -pkg the name for the generated package (default: slogx) 172 | -i add import 173 | -l add level 174 | -c add constant 175 | -a add attribute 176 | -logger generate a custom Logger type 177 | -api the API style for the Logger's methods (default: any) 178 | -ctx add context.Context to the Logger's methods 179 | -h, -help print this message and quit 180 | ``` 181 | 182 | For the description of the config file fields, see [`.slog.example.yml`](.slog.example.yml). 183 | 184 | [1]: https://github.com/go-simpler/sloglint 185 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-simpler.org/sloggen. DO NOT EDIT. 2 | 3 | package example 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log/slog" 9 | "runtime" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | const LevelInfo = slog.Level(0) 15 | const LevelAlert = slog.Level(12) 16 | 17 | const RequestId = "request_id" 18 | 19 | func CreatedAt(value time.Time) slog.Attr { return slog.Time("created_at", value) } 20 | func Err(value error) slog.Attr { return slog.Any("err", value) } 21 | func UserId(value int) slog.Attr { return slog.Int("user_id", value) } 22 | 23 | func ParseLevel(s string) (slog.Level, error) { 24 | switch strings.ToUpper(s) { 25 | case "INFO": 26 | return LevelInfo, nil 27 | case "ALERT": 28 | return LevelAlert, nil 29 | default: 30 | return 0, fmt.Errorf("slog: level string %q: unknown name", s) 31 | } 32 | } 33 | 34 | func RenameLevels(_ []string, attr slog.Attr) slog.Attr { 35 | if attr.Key != slog.LevelKey { 36 | return attr 37 | } 38 | switch attr.Value.Any().(slog.Level) { 39 | case LevelInfo: 40 | attr.Value = slog.StringValue("INFO") 41 | case LevelAlert: 42 | attr.Value = slog.StringValue("ALERT") 43 | } 44 | return attr 45 | } 46 | 47 | type Logger struct{ handler slog.Handler } 48 | 49 | func New(h slog.Handler) *Logger { return &Logger{handler: h} } 50 | 51 | func (l *Logger) Handler() slog.Handler { return l.handler } 52 | 53 | func (l *Logger) Enabled(ctx context.Context, level slog.Level) bool { 54 | return l.handler.Enabled(ctx, level) 55 | } 56 | 57 | func (l *Logger) With(attrs ...slog.Attr) *Logger { 58 | if len(attrs) == 0 { 59 | return l 60 | } 61 | return &Logger{handler: l.handler.WithAttrs(attrs)} 62 | } 63 | 64 | func (l *Logger) WithGroup(name string) *Logger { 65 | if name == "" { 66 | return l 67 | } 68 | return &Logger{handler: l.handler.WithGroup(name)} 69 | } 70 | 71 | func (l *Logger) Log(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) { 72 | l.log(ctx, level, msg, attrs) 73 | } 74 | 75 | func (l *Logger) Info(ctx context.Context, msg string, attrs ...slog.Attr) { 76 | l.log(ctx, LevelInfo, msg, attrs) 77 | } 78 | 79 | func (l *Logger) Alert(ctx context.Context, msg string, attrs ...slog.Attr) { 80 | l.log(ctx, LevelAlert, msg, attrs) 81 | } 82 | 83 | func (l *Logger) log(ctx context.Context, level slog.Level, msg string, attrs []slog.Attr) { 84 | if !l.handler.Enabled(ctx, level) { 85 | return 86 | } 87 | var pcs [1]uintptr 88 | runtime.Callers(3, pcs[:]) 89 | r := slog.NewRecord(time.Now(), level, msg, pcs[0]) 90 | r.AddAttrs(attrs...) 91 | _ = l.handler.Handle(ctx, r) 92 | } 93 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NOTE: replace "go run main.go sloggen.go" with "go run go-simpler.org/sloggen@" in your project. 4 | 5 | // using flags: 6 | //go:generate go run main.go sloggen.go -pkg=example -i=time -l=info:0 -l=alert:12 -c=request_id -a=user_id:int -a=created_at:time.Time -a=err:error -logger -api=attr -ctx 7 | 8 | // using config (see .slog.example.yml): 9 | //go:generate go run main.go sloggen.go -config=.slog.example.yml 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-simpler.org/sloggen 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | go-simpler.org/assert v0.9.0 7 | go-simpler.org/errorsx v0.10.0 8 | golang.org/x/tools v0.31.0 9 | gopkg.in/yaml.v3 v3.0.1 10 | ) 11 | 12 | require ( 13 | golang.org/x/mod v0.24.0 // indirect 14 | golang.org/x/sync v0.12.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= 4 | go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= 5 | go-simpler.org/errorsx v0.10.0 h1:w8Uf7LnoOzd1M4+fj9h39hR/AMKh2+IwDu+LnRierEU= 6 | go-simpler.org/errorsx v0.10.0/go.mod h1:Mh3dw+eRSRHll1apcEmXjSps8K0fAWMaNlGWEnOaiNU= 7 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 8 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 9 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 10 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 11 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 12 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | "go-simpler.org/errorsx" 11 | ) 12 | 13 | func main() { 14 | if err := run(); err != nil { 15 | fmt.Println(err) 16 | os.Exit(1) 17 | } 18 | } 19 | 20 | func run() (runErr error) { 21 | cfg, err := readFlags(os.Args[1:]) 22 | if err != nil { 23 | if errors.Is(err, flag.ErrHelp) { 24 | return nil 25 | } 26 | return err 27 | } 28 | 29 | path, dir := cfg.path, cfg.dir 30 | 31 | if path != "" { 32 | cfgFile, err := os.Open(path) 33 | if err != nil { 34 | return err 35 | } 36 | defer errorsx.Close(cfgFile, &runErr) 37 | 38 | cfg, err = readConfig(cfgFile) 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | 44 | if dir != "" { 45 | if err := os.Chdir(dir); err != nil { 46 | return err 47 | } 48 | } 49 | 50 | if err := os.Mkdir(cfg.Pkg, 0o755); err != nil && !errors.Is(err, os.ErrExist) { 51 | return err 52 | } 53 | 54 | genFile, err := os.Create(filepath.Join(cfg.Pkg, cfg.Pkg+".go")) 55 | if err != nil { 56 | return err 57 | } 58 | defer errorsx.Close(genFile, &runErr) 59 | 60 | return writeCode(genFile, cfg) 61 | } 62 | -------------------------------------------------------------------------------- /sloggen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "slices" 11 | "strconv" 12 | "strings" 13 | "text/template" 14 | 15 | "golang.org/x/tools/imports" 16 | "gopkg.in/yaml.v3" 17 | ) 18 | 19 | var ( 20 | //go:embed template.go.tmpl 21 | src string 22 | tmpl = template.Must(template.New("").Funcs(funcs).Parse(src)) 23 | 24 | //nolint:staticcheck // SA1019: strings.Title is deprecated but works just fine here. 25 | funcs = template.FuncMap{ 26 | "title": strings.Title, 27 | "upper": strings.ToUpper, 28 | "camel": func(s string) string { 29 | parts := strings.Split(s, "_") 30 | for i := range parts { 31 | parts[i] = strings.Title(parts[i]) 32 | } 33 | return strings.Join(parts, "") 34 | }, 35 | "slogFunc": func(typ string) string { 36 | switch s := strings.Title(strings.TrimPrefix(typ, "time.")); s { 37 | case "String", "Int64", "Int", "Uint64", "Float64", "Bool", "Time", "Duration": 38 | return s 39 | default: 40 | return "Any" 41 | } 42 | }, 43 | } 44 | ) 45 | 46 | // NOTE: when iterating over a map, text/template visits the elements in sorted key order. 47 | type ( 48 | config struct { 49 | Pkg string 50 | Imports []string 51 | Levels map[int]string // severity:name 52 | Consts []string 53 | Attrs map[string]string // key:type 54 | Logger *logger 55 | path, dir string // for flags only. 56 | } 57 | logger struct { 58 | Levels map[int]string 59 | AttrAPI bool 60 | Context bool 61 | } 62 | ) 63 | 64 | func (c *config) prepare() { 65 | if c.Pkg == "" { 66 | c.Pkg = "slogx" 67 | } 68 | if len(c.Levels) > 0 { 69 | c.Imports = append(c.Imports, "log/slog", "fmt", "strings") 70 | } 71 | if len(c.Attrs) > 0 { 72 | c.Imports = append(c.Imports, "log/slog") 73 | } 74 | if c.Logger != nil { 75 | c.Imports = append(c.Imports, "log/slog", "context", "runtime") 76 | c.Logger.Levels = c.Levels 77 | if len(c.Levels) == 0 { 78 | c.Logger.Levels = map[int]string{ 79 | int(slog.LevelDebug): "debug", 80 | int(slog.LevelInfo): "info", 81 | int(slog.LevelWarn): "warn", 82 | int(slog.LevelError): "error", 83 | } 84 | } 85 | } 86 | slices.Sort(c.Consts) 87 | slices.Sort(c.Imports) 88 | c.Imports = slices.Compact(c.Imports) 89 | } 90 | 91 | func readFlags(args []string) (*config, error) { 92 | cfg := config{ 93 | Levels: make(map[int]string), 94 | Attrs: make(map[string]string), 95 | } 96 | 97 | fs := flag.NewFlagSet("sloggen", flag.ContinueOnError) 98 | fs.SetOutput(io.Discard) 99 | fs.Usage = func() { 100 | fmt.Println(`Usage: sloggen [flags] 101 | 102 | Flags: 103 | -config read config from the file instead of flags 104 | -dir change the working directory before generating files 105 | -pkg the name for the generated package (default: slogx) 106 | -i add import 107 | -l add level 108 | -c add constant 109 | -a add attribute 110 | -logger generate a custom Logger type 111 | -api the API style for the Logger's methods (default: any) 112 | -ctx add context.Context to the Logger's methods 113 | -h, -help print this message and quit`) 114 | } 115 | 116 | fs.StringVar(&cfg.path, "config", "", "") 117 | fs.StringVar(&cfg.dir, "dir", "", "") 118 | fs.StringVar(&cfg.Pkg, "pkg", "", "") 119 | 120 | fs.Func("i", "", func(s string) error { 121 | cfg.Imports = append(cfg.Imports, s) 122 | return nil 123 | }) 124 | fs.Func("l", "", func(s string) error { 125 | parts := strings.Split(s, ":") 126 | if len(parts) != 2 { 127 | return fmt.Errorf("sloggen: -l=%s: invalid value", s) 128 | } 129 | severity, err := strconv.Atoi(parts[1]) 130 | if err != nil { 131 | return fmt.Errorf("parsing severity: %w", err) 132 | } 133 | cfg.Levels[severity] = parts[0] 134 | return nil 135 | }) 136 | fs.Func("c", "", func(s string) error { 137 | cfg.Consts = append(cfg.Consts, s) 138 | return nil 139 | }) 140 | fs.Func("a", "", func(s string) error { 141 | parts := strings.Split(s, ":") 142 | if len(parts) != 2 { 143 | return fmt.Errorf("sloggen: -a=%s: invalid value", s) 144 | } 145 | cfg.Attrs[parts[0]] = parts[1] 146 | return nil 147 | }) 148 | fs.BoolFunc("logger", "", func(string) error { 149 | cfg.Logger = new(logger) 150 | return nil 151 | }) 152 | fs.Func("api", "", func(s string) error { 153 | if s != "any" && s != "attr" { 154 | return fmt.Errorf("sloggen: -api=%s: invalid value", s) 155 | } 156 | if cfg.Logger != nil { 157 | cfg.Logger.AttrAPI = s == "attr" 158 | } 159 | return nil 160 | }) 161 | fs.BoolFunc("ctx", "", func(string) error { 162 | if cfg.Logger != nil { 163 | cfg.Logger.Context = true 164 | } 165 | return nil 166 | }) 167 | 168 | if err := fs.Parse(args); err != nil { 169 | return nil, fmt.Errorf("parsing flags: %w", err) 170 | } 171 | 172 | cfg.prepare() 173 | return &cfg, nil 174 | } 175 | 176 | func readConfig(r io.Reader) (*config, error) { 177 | var data struct { 178 | Pkg string `yaml:"pkg"` 179 | Imports []string `yaml:"imports"` 180 | Levels []map[string]int `yaml:"levels"` 181 | Consts []string `yaml:"consts"` 182 | Attrs []map[string]string `yaml:"attrs"` 183 | Logger *struct { 184 | API string `yaml:"api"` 185 | Ctx bool `yaml:"ctx"` 186 | } `yaml:"logger"` 187 | } 188 | if err := yaml.NewDecoder(r).Decode(&data); err != nil { 189 | return nil, fmt.Errorf("decoding config: %w", err) 190 | } 191 | 192 | cfg := config{ 193 | Pkg: data.Pkg, 194 | Imports: data.Imports, 195 | Levels: make(map[int]string, len(data.Levels)), 196 | Consts: data.Consts, 197 | Attrs: make(map[string]string, len(data.Attrs)), 198 | } 199 | 200 | for _, m := range data.Levels { 201 | name, severity := firstKV(m) 202 | cfg.Levels[severity] = name 203 | } 204 | for _, m := range data.Attrs { 205 | key, typ := firstKV(m) 206 | cfg.Attrs[key] = typ 207 | } 208 | if data.Logger != nil { 209 | if data.Logger.API != "any" && data.Logger.API != "attr" { 210 | return nil, fmt.Errorf("sloggen: logger.api=%s: invalid value", data.Logger.API) 211 | } 212 | cfg.Logger = &logger{ 213 | AttrAPI: data.Logger.API == "attr", 214 | Context: data.Logger.Ctx, 215 | } 216 | } 217 | 218 | cfg.prepare() 219 | return &cfg, nil 220 | } 221 | 222 | func writeCode(w io.Writer, cfg *config) error { 223 | var buf bytes.Buffer 224 | if err := tmpl.Execute(&buf, cfg); err != nil { 225 | return fmt.Errorf("executing template: %w", err) 226 | } 227 | src, err := imports.Process("", buf.Bytes(), nil) 228 | if err != nil { 229 | return fmt.Errorf("formatting code: %w", err) 230 | } 231 | if _, err := w.Write(src); err != nil { 232 | return fmt.Errorf("writing code: %w", err) 233 | } 234 | return nil 235 | } 236 | 237 | //nolint:gocritic // unnamedResult: generics false positive. 238 | func firstKV[V any](m map[string]V) (string, V) { 239 | for k, v := range m { 240 | return k, v 241 | } 242 | return "", *new(V) 243 | } 244 | -------------------------------------------------------------------------------- /sloggen_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "log/slog" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "go-simpler.org/assert" 12 | . "go-simpler.org/assert/EF" 13 | "go-simpler.org/sloggen/example" 14 | ) 15 | 16 | var cfg = config{ 17 | Pkg: "test", 18 | Imports: []string{"fmt", "log/slog", "strings", "time"}, 19 | Levels: map[int]string{1: "custom"}, 20 | Consts: []string{"foo"}, 21 | Attrs: map[string]string{ 22 | "bar": "time.Time", 23 | "baz": "time.Duration", 24 | }, 25 | } 26 | 27 | func Test_readFlags(t *testing.T) { 28 | args := []string{ 29 | "-pkg=test", 30 | "-i=time", 31 | "-l=custom:1", 32 | "-c=foo", 33 | "-a=bar:time.Time", 34 | "-a=baz:time.Duration", 35 | } 36 | 37 | got, err := readFlags(args) 38 | assert.NoErr[F](t, err) 39 | assert.Equal[E](t, got, &cfg) 40 | } 41 | 42 | func Test_readConfig(t *testing.T) { 43 | r := strings.NewReader(` 44 | pkg: test 45 | imports: 46 | - time 47 | levels: 48 | - custom: 1 49 | consts: 50 | - foo 51 | attrs: 52 | - bar: time.Time 53 | - baz: time.Duration 54 | `) 55 | 56 | got, err := readConfig(r) 57 | assert.NoErr[F](t, err) 58 | assert.Equal[E](t, got, &cfg) 59 | } 60 | 61 | func Test_writeCode(t *testing.T) { 62 | const src = `// Code generated by go-simpler.org/sloggen. DO NOT EDIT. 63 | 64 | package test 65 | 66 | import ( 67 | "fmt" 68 | "log/slog" 69 | "strings" 70 | "time" 71 | ) 72 | 73 | const LevelCustom = slog.Level(1) 74 | 75 | const Foo = "foo" 76 | 77 | func Bar(value time.Time) slog.Attr { return slog.Time("bar", value) } 78 | func Baz(value time.Duration) slog.Attr { return slog.Duration("baz", value) } 79 | 80 | func ParseLevel(s string) (slog.Level, error) { 81 | switch strings.ToUpper(s) { 82 | case "CUSTOM": 83 | return LevelCustom, nil 84 | default: 85 | return 0, fmt.Errorf("slog: level string %q: unknown name", s) 86 | } 87 | } 88 | 89 | func RenameLevels(_ []string, attr slog.Attr) slog.Attr { 90 | if attr.Key != slog.LevelKey { 91 | return attr 92 | } 93 | switch attr.Value.Any().(slog.Level) { 94 | case LevelCustom: 95 | attr.Value = slog.StringValue("CUSTOM") 96 | } 97 | return attr 98 | } 99 | ` 100 | var buf bytes.Buffer 101 | err := writeCode(&buf, &cfg) 102 | assert.NoErr[F](t, err) 103 | assert.Equal[E](t, buf.String(), src) 104 | } 105 | 106 | func TestExample(t *testing.T) { 107 | replaceAttr := func(groups []string, attr slog.Attr) slog.Attr { 108 | if attr.Key == slog.TimeKey { 109 | return slog.Attr{} 110 | } 111 | if attr.Key == slog.SourceKey { 112 | src := attr.Value.Any().(*slog.Source) 113 | src.File = filepath.Base(src.File) 114 | } 115 | return example.RenameLevels(groups, attr) 116 | } 117 | 118 | var buf bytes.Buffer 119 | handler := slog.NewTextHandler(&buf, &slog.HandlerOptions{ 120 | AddSource: true, 121 | Level: example.LevelInfo, 122 | ReplaceAttr: replaceAttr, 123 | }) 124 | 125 | logger := example.New(handler). 126 | WithGroup("group"). 127 | With(slog.String("key", "value")) 128 | 129 | level, err := example.ParseLevel("ALERT") 130 | assert.NoErr[F](t, err) 131 | assert.Equal[E](t, level, example.LevelAlert) 132 | 133 | ctx := context.Background() 134 | enabled := logger.Enabled(ctx, level) 135 | assert.Equal[E](t, enabled, true) 136 | 137 | logger.Info(ctx, "foo") 138 | logger.Alert(ctx, "bar") 139 | logger.Log(ctx, level, "baz") 140 | assert.Equal[E](t, "\n"+buf.String(), ` 141 | level=INFO source=sloggen_test.go:137 msg=foo group.key=value 142 | level=ALERT source=sloggen_test.go:138 msg=bar group.key=value 143 | level=ALERT source=sloggen_test.go:139 msg=baz group.key=value 144 | `) 145 | } 146 | -------------------------------------------------------------------------------- /template.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by go-simpler.org/sloggen. DO NOT EDIT. 2 | 3 | package {{$.Pkg}} 4 | 5 | {{range $.Imports -}} 6 | import "{{.}}" 7 | {{end}} 8 | 9 | {{range $severity, $name := $.Levels -}} 10 | const Level{{title $name}} = slog.Level({{$severity}}) 11 | {{end}} 12 | 13 | {{range $.Consts -}} 14 | const {{camel .}} = "{{.}}" 15 | {{end}} 16 | 17 | {{range $key, $type := $.Attrs -}} 18 | func {{camel $key}}(value {{$type}}) slog.Attr { return slog.{{slogFunc $type}}("{{$key}}", value) } 19 | {{end}} 20 | 21 | {{if gt (len $.Levels) 0}} 22 | func ParseLevel(s string) (slog.Level, error) { 23 | switch strings.ToUpper(s) { 24 | {{range $_, $name := $.Levels -}} 25 | case "{{upper $name}}": 26 | return Level{{title $name}}, nil 27 | {{end -}} 28 | default: 29 | return 0, fmt.Errorf("slog: level string %q: unknown name", s) 30 | } 31 | } 32 | 33 | func RenameLevels(_ []string, attr slog.Attr) slog.Attr { 34 | if attr.Key != slog.LevelKey { 35 | return attr 36 | } 37 | switch attr.Value.Any().(slog.Level) { 38 | {{range $_, $name := $.Levels -}} 39 | case Level{{title $name}}: 40 | attr.Value = slog.StringValue("{{upper $name}}") 41 | {{end -}} 42 | } 43 | return attr 44 | } 45 | {{end}} 46 | 47 | {{if $l := $.Logger}} 48 | type Logger struct{ handler slog.Handler } 49 | 50 | func New(h slog.Handler) *Logger { return &Logger{handler: h} } 51 | 52 | func (l *Logger) Handler() slog.Handler { return l.handler } 53 | 54 | func (l *Logger) Enabled(ctx context.Context, level slog.Level) bool { 55 | return l.handler.Enabled(ctx, level) 56 | } 57 | 58 | func (l *Logger) With({{if $l.AttrAPI}}attrs ...slog.Attr{{else}}args ...any{{end}}) *Logger { 59 | if len({{if $l.AttrAPI}}attrs{{else}}args{{end}}) == 0 { 60 | return l 61 | } 62 | return &Logger{handler: l.handler.WithAttrs({{if $l.AttrAPI}}attrs{{else}}args2attrs(args){{end}})} 63 | } 64 | 65 | func (l *Logger) WithGroup(name string) *Logger { 66 | if name == "" { 67 | return l 68 | } 69 | return &Logger{handler: l.handler.WithGroup(name)} 70 | } 71 | 72 | func (l *Logger) Log({{if $l.Context}}ctx context.Context, {{end}}level slog.Level, msg string, {{if $l.AttrAPI}}attrs ...slog.Attr{{else}}args ...any{{end}}) { 73 | l.log({{if $l.Context}}ctx{{else}}context.Background(){{end}}, level, msg, {{if $l.AttrAPI}}attrs{{else}}args{{end}}) 74 | } 75 | 76 | {{range $_, $name := $l.Levels}} 77 | func (l *Logger) {{title $name}}({{if $l.Context}}ctx context.Context, {{end}}msg string, {{if $l.AttrAPI}}attrs ...slog.Attr{{else}}args ...any{{end}}) { 78 | l.log({{if $l.Context}}ctx{{else}}context.Background(){{end}}, {{if eq (len $.Levels) 0}}slog.{{end}}Level{{title $name}}, msg, {{if $l.AttrAPI}}attrs{{else}}args{{end}}) 79 | } 80 | {{end}} 81 | 82 | func (l *Logger) log(ctx context.Context, level slog.Level, msg string, {{if $l.AttrAPI}}attrs []slog.Attr{{else}}args []any{{end}}) { 83 | if !l.handler.Enabled(ctx, level) { 84 | return 85 | } 86 | var pcs [1]uintptr 87 | runtime.Callers(3, pcs[:]) 88 | r := slog.NewRecord(time.Now(), level, msg, pcs[0]) 89 | r.Add{{if $l.AttrAPI}}Attrs(attrs...){{else}}(args...){{end}} 90 | _ = l.handler.Handle(ctx, r) 91 | } 92 | 93 | {{if not $l.AttrAPI}} 94 | {{/* based on argsToAttrSlice() and argsToAttr() from log/slog sources. */}} 95 | func args2attrs(args []any) []slog.Attr { 96 | var attrs []slog.Attr 97 | for len(args) > 0 { 98 | switch x := args[0].(type) { 99 | case string: 100 | if len(args) == 1 { 101 | attrs, args = append(attrs, slog.String("!BADKEY", x)), nil 102 | } else { 103 | attrs, args = append(attrs, slog.Any(x, args[1])), args[2:] 104 | } 105 | case slog.Attr: 106 | attrs, args = append(attrs, x), args[1:] 107 | default: 108 | attrs, args = append(attrs, slog.Any("!BADKEY", x)), args[1:] 109 | } 110 | } 111 | return attrs 112 | } 113 | {{end}} 114 | {{end}} 115 | --------------------------------------------------------------------------------