Initial QSfera import
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/go
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=go
|
||||
|
||||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go
|
||||
|
||||
cover.out
|
||||
cover.html
|
||||
.vscode
|
||||
|
||||
.idea/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Samuel Berthe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
|
||||
build:
|
||||
go build -v ./...
|
||||
|
||||
test:
|
||||
go test -race -v ./...
|
||||
watch-test:
|
||||
reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
|
||||
|
||||
bench:
|
||||
go test -benchmem -count 3 -bench ./...
|
||||
watch-bench:
|
||||
reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...'
|
||||
|
||||
fuzz:
|
||||
go test -fuzz=FuzzAttrsToMap -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzValueToString -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzAnyValueToString -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzFindAttribute -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzRemoveEmptyAttrs -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzUniqAttrs -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzAttrsToString -fuzztime=10s ./...
|
||||
|
||||
coverage:
|
||||
go test -v -coverprofile=cover.out -covermode=atomic ./...
|
||||
go tool cover -html=cover.out -o cover.html
|
||||
|
||||
tools:
|
||||
go install github.com/cespare/reflex@latest
|
||||
go install github.com/rakyll/gotest@latest
|
||||
go install github.com/psampaz/go-mod-outdated@latest
|
||||
go install github.com/jondot/goweight@latest
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
go get -t -u golang.org/x/tools/cmd/cover
|
||||
go install github.com/sonatype-nexus-community/nancy@latest
|
||||
go mod tidy
|
||||
|
||||
lint:
|
||||
golangci-lint run --timeout 60s --max-same-issues 50 ./...
|
||||
lint-fix:
|
||||
golangci-lint run --timeout 60s --max-same-issues 50 --fix ./...
|
||||
|
||||
audit:
|
||||
go list -json -m all | nancy sleuth
|
||||
|
||||
outdated:
|
||||
go list -u -m -json all | go-mod-outdated -update -direct
|
||||
|
||||
weight:
|
||||
goweight
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
|
||||
# Nothing to see here (internal package)
|
||||
|
||||
[](https://github.com/samber/slog-common/releases)
|
||||

|
||||
[](https://pkg.go.dev/github.com/samber/slog-common)
|
||||

|
||||
[](https://goreportcard.com/report/github.com/samber/slog-common)
|
||||
[](https://codecov.io/gh/samber/slog-common)
|
||||
[](https://github.com/samber/slog-common/graphs/contributors)
|
||||
[](./LICENSE)
|
||||
|
||||

|
||||
|
||||
A toolchain for [slog](https://pkg.go.dev/log/slog) Go library.
|
||||
|
||||
This project gathers common functions for my [slog](https://pkg.go.dev/log/slog) Go libraries:
|
||||
|
||||
<div align="center">
|
||||
<hr>
|
||||
<sup><b>Sponsored by:</b></sup>
|
||||
<br>
|
||||
<a href="https://cast.ai/samuel">
|
||||
<div>
|
||||
<img src="https://github.com/user-attachments/assets/502f8fa8-e7e8-4754-a51f-036d0443e694" width="200" alt="Cast AI">
|
||||
</div>
|
||||
<div>
|
||||
Cut Kubernetes & AI costs, boost application stability
|
||||
</div>
|
||||
</a>
|
||||
<br>
|
||||
<a href="https://www.dash0.com?utm_campaign=148395251-samber%20github%20sponsorship&utm_source=github&utm_medium=sponsorship&utm_content=samber">
|
||||
<div>
|
||||
<img src="https://github.com/user-attachments/assets/b1f2e876-0954-4dc3-824d-935d29ba8f3f" width="200" alt="Dash0">
|
||||
</div>
|
||||
<div>
|
||||
100% OpenTelemetry-native observability platform<br>Simple to use, built on open standards, and designed for full cost control
|
||||
</div>
|
||||
</a>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
**See also:**
|
||||
|
||||
- [slog-multi](https://github.com/samber/slog-multi): `slog.Handler` chaining, fanout, routing, failover, load balancing...
|
||||
- [slog-formatter](https://github.com/samber/slog-formatter): `slog` attribute formatting
|
||||
- [slog-sampling](https://github.com/samber/slog-sampling): `slog` sampling policy
|
||||
- [slog-mock](https://github.com/samber/slog-mock): `slog.Handler` for test purposes
|
||||
|
||||
**HTTP middlewares:**
|
||||
|
||||
- [slog-gin](https://github.com/samber/slog-gin): Gin middleware for `slog` logger
|
||||
- [slog-echo](https://github.com/samber/slog-echo): Echo middleware for `slog` logger
|
||||
- [slog-fiber](https://github.com/samber/slog-fiber): Fiber middleware for `slog` logger
|
||||
- [slog-chi](https://github.com/samber/slog-chi): Chi middleware for `slog` logger
|
||||
- [slog-http](https://github.com/samber/slog-http): `net/http` middleware for `slog` logger
|
||||
|
||||
**Loggers:**
|
||||
|
||||
- [slog-zap](https://github.com/samber/slog-zap): A `slog` handler for `Zap`
|
||||
- [slog-zerolog](https://github.com/samber/slog-zerolog): A `slog` handler for `Zerolog`
|
||||
- [slog-logrus](https://github.com/samber/slog-logrus): A `slog` handler for `Logrus`
|
||||
|
||||
**Log sinks:**
|
||||
|
||||
- [slog-datadog](https://github.com/samber/slog-datadog): A `slog` handler for `Datadog`
|
||||
- [slog-betterstack](https://github.com/samber/slog-betterstack): A `slog` handler for `Betterstack`
|
||||
- [slog-rollbar](https://github.com/samber/slog-rollbar): A `slog` handler for `Rollbar`
|
||||
- [slog-loki](https://github.com/samber/slog-loki): A `slog` handler for `Loki`
|
||||
- [slog-sentry](https://github.com/samber/slog-sentry): A `slog` handler for `Sentry`
|
||||
- [slog-syslog](https://github.com/samber/slog-syslog): A `slog` handler for `Syslog`
|
||||
- [slog-logstash](https://github.com/samber/slog-logstash): A `slog` handler for `Logstash`
|
||||
- [slog-fluentd](https://github.com/samber/slog-fluentd): A `slog` handler for `Fluentd`
|
||||
- [slog-graylog](https://github.com/samber/slog-graylog): A `slog` handler for `Graylog`
|
||||
- [slog-quickwit](https://github.com/samber/slog-quickwit): A `slog` handler for `Quickwit`
|
||||
- [slog-slack](https://github.com/samber/slog-slack): A `slog` handler for `Slack`
|
||||
- [slog-telegram](https://github.com/samber/slog-telegram): A `slog` handler for `Telegram`
|
||||
- [slog-mattermost](https://github.com/samber/slog-mattermost): A `slog` handler for `Mattermost`
|
||||
- [slog-microsoft-teams](https://github.com/samber/slog-microsoft-teams): A `slog` handler for `Microsoft Teams`
|
||||
- [slog-webhook](https://github.com/samber/slog-webhook): A `slog` handler for `Webhook`
|
||||
- [slog-kafka](https://github.com/samber/slog-kafka): A `slog` handler for `Kafka`
|
||||
- [slog-nats](https://github.com/samber/slog-nats): A `slog` handler for `NATS`
|
||||
- [slog-parquet](https://github.com/samber/slog-parquet): A `slog` handler for `Parquet` + `Object Storage`
|
||||
- [slog-channel](https://github.com/samber/slog-channel): A `slog` handler for Go channels
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
- Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :))
|
||||
- Fork the [project](https://github.com/samber/slog-common)
|
||||
- Fix [open issues](https://github.com/samber/slog-common/issues) or request new features
|
||||
|
||||
Don't hesitate ;)
|
||||
|
||||
```bash
|
||||
# Install some dev dependencies
|
||||
make tools
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
# or
|
||||
make watch-test
|
||||
```
|
||||
|
||||
## 👤 Contributors
|
||||
|
||||

|
||||
|
||||
## 💫 Show your support
|
||||
|
||||
Give a ⭐️ if this project helped you!
|
||||
|
||||
[](https://github.com/sponsors/samber)
|
||||
|
||||
## 📝 License
|
||||
|
||||
Copyright © 2023 [Samuel Berthe](https://github.com/samber).
|
||||
|
||||
This project is [MIT](./LICENSE) licensed.
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
package slogcommon
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type ReplaceAttrFn = func(groups []string, a slog.Attr) slog.Attr
|
||||
|
||||
func AppendRecordAttrsToAttrs(attrs []slog.Attr, groups []string, record *slog.Record) []slog.Attr {
|
||||
output := make([]slog.Attr, 0, len(attrs)+record.NumAttrs())
|
||||
output = append(output, attrs...)
|
||||
|
||||
record.Attrs(func(attr slog.Attr) bool {
|
||||
for i := len(groups) - 1; i >= 0; i-- {
|
||||
attr = slog.Group(groups[i], attr)
|
||||
}
|
||||
output = append(output, attr)
|
||||
return true
|
||||
})
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func ReplaceAttrs(fn ReplaceAttrFn, groups []string, attrs ...slog.Attr) []slog.Attr {
|
||||
for i := range attrs {
|
||||
attr := attrs[i]
|
||||
value := attr.Value.Resolve()
|
||||
if value.Kind() == slog.KindGroup {
|
||||
attrs[i].Value = slog.GroupValue(ReplaceAttrs(fn, append(groups, attr.Key), value.Group()...)...)
|
||||
} else if fn != nil {
|
||||
attrs[i] = fn(groups, attr)
|
||||
} else {
|
||||
attrs[i].Value = value
|
||||
}
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
func AttrsToMap(attrs ...slog.Attr) map[string]any {
|
||||
output := map[string]any{}
|
||||
|
||||
attrsByKey := groupValuesByKey(attrs)
|
||||
for k, values := range attrsByKey {
|
||||
v := mergeAttrValues(values...)
|
||||
if v.Kind() == slog.KindGroup {
|
||||
output[k] = AttrsToMap(v.Group()...)
|
||||
} else {
|
||||
output[k] = v.Any()
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func RecordToAttrsMap(r slog.Record) map[string]any {
|
||||
attrs := make([]slog.Attr, 0, r.NumAttrs())
|
||||
r.Attrs(func(attr slog.Attr) bool {
|
||||
attrs = append(attrs, attr)
|
||||
return true
|
||||
})
|
||||
|
||||
return AttrsToMap(attrs...)
|
||||
}
|
||||
|
||||
func groupValuesByKey(attrs []slog.Attr) map[string][]slog.Value {
|
||||
result := map[string][]slog.Value{}
|
||||
|
||||
for _, item := range attrs {
|
||||
key := item.Key
|
||||
result[key] = append(result[key], item.Value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeAttrValues(values ...slog.Value) slog.Value {
|
||||
v := values[0]
|
||||
|
||||
for i := 1; i < len(values); i++ {
|
||||
if v.Kind() != slog.KindGroup || values[i].Kind() != slog.KindGroup {
|
||||
v = values[i]
|
||||
continue
|
||||
}
|
||||
|
||||
v = slog.GroupValue(append(v.Group(), values[i].Group()...)...)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func AttrToValue(attr slog.Attr) (string, any) {
|
||||
k := attr.Key
|
||||
v := attr.Value
|
||||
kind := v.Kind()
|
||||
|
||||
switch kind {
|
||||
case slog.KindAny:
|
||||
return k, v.Any()
|
||||
case slog.KindLogValuer:
|
||||
return k, v.Any()
|
||||
case slog.KindGroup:
|
||||
return k, AttrsToMap(v.Group()...)
|
||||
case slog.KindInt64:
|
||||
return k, v.Int64()
|
||||
case slog.KindUint64:
|
||||
return k, v.Uint64()
|
||||
case slog.KindFloat64:
|
||||
return k, v.Float64()
|
||||
case slog.KindString:
|
||||
return k, v.String()
|
||||
case slog.KindBool:
|
||||
return k, v.Bool()
|
||||
case slog.KindDuration:
|
||||
return k, v.Duration()
|
||||
case slog.KindTime:
|
||||
return k, v.Time().UTC()
|
||||
default:
|
||||
return k, AnyValueToString(v)
|
||||
}
|
||||
}
|
||||
|
||||
func AnyValueToString(v slog.Value) string {
|
||||
if tm, ok := v.Any().(encoding.TextMarshaler); ok {
|
||||
data, err := tm.MarshalText()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%+v", v.Any())
|
||||
}
|
||||
|
||||
func AttrsToString(attrs ...slog.Attr) map[string]string {
|
||||
output := make(map[string]string, len(attrs))
|
||||
|
||||
for i := range attrs {
|
||||
attr := attrs[i]
|
||||
k, v := attr.Key, attr.Value
|
||||
output[k] = ValueToString(v)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func ValueToString(v slog.Value) string {
|
||||
switch v.Kind() {
|
||||
case slog.KindAny, slog.KindLogValuer, slog.KindGroup:
|
||||
return AnyValueToString(v)
|
||||
case slog.KindInt64, slog.KindUint64, slog.KindFloat64, slog.KindString, slog.KindBool, slog.KindDuration:
|
||||
return v.String()
|
||||
case slog.KindTime:
|
||||
return v.Time().UTC().String()
|
||||
default:
|
||||
return AnyValueToString(v)
|
||||
}
|
||||
}
|
||||
|
||||
func ReplaceError(attrs []slog.Attr, errorKeys ...string) []slog.Attr {
|
||||
replaceAttr := func(groups []string, a slog.Attr) slog.Attr {
|
||||
if len(groups) > 1 {
|
||||
return a
|
||||
}
|
||||
|
||||
for i := range errorKeys {
|
||||
if a.Key == errorKeys[i] {
|
||||
if err, ok := a.Value.Any().(error); ok {
|
||||
return slog.Any(a.Key, FormatError(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
return ReplaceAttrs(replaceAttr, []string{}, attrs...)
|
||||
}
|
||||
|
||||
func ExtractError(attrs []slog.Attr, errorKeys ...string) ([]slog.Attr, error) {
|
||||
for i := range attrs {
|
||||
attr := attrs[i]
|
||||
|
||||
if !slices.Contains(errorKeys, attr.Key) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err, ok := attr.Value.Resolve().Any().(error); ok {
|
||||
output := make([]slog.Attr, 0, len(attrs)-1)
|
||||
output = append(output, attrs[:i]...)
|
||||
output = append(output, attrs[i+1:]...)
|
||||
return output, err
|
||||
}
|
||||
}
|
||||
|
||||
return attrs, nil
|
||||
}
|
||||
|
||||
func FormatErrorKey(values map[string]any, errorKeys ...string) map[string]any {
|
||||
for _, errorKey := range errorKeys {
|
||||
if err, ok := values[errorKey]; ok {
|
||||
if e, ok := err.(error); ok {
|
||||
values[errorKey] = FormatError(e)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func FormatError(err error) any {
|
||||
if e, ok := err.(slog.LogValuer); ok {
|
||||
return e.LogValue()
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"kind": reflect.TypeOf(err).String(),
|
||||
"error": err.Error(),
|
||||
"stack": nil, // @TODO
|
||||
}
|
||||
}
|
||||
|
||||
func FormatRequest(req *http.Request, ignoreHeaders bool) map[string]any {
|
||||
output := map[string]any{
|
||||
"host": req.Host,
|
||||
"method": req.Method,
|
||||
"url": map[string]any{
|
||||
"url": req.URL.String(),
|
||||
"scheme": req.URL.Scheme,
|
||||
"host": req.URL.Host,
|
||||
"path": req.URL.Path,
|
||||
"raw_query": req.URL.RawQuery,
|
||||
"fragment": req.URL.Fragment,
|
||||
"query": lo.MapEntries(req.URL.Query(), func(key string, values []string) (string, string) {
|
||||
return key, strings.Join(values, ",")
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
if !ignoreHeaders {
|
||||
output["headers"] = lo.MapEntries(req.Header, func(key string, values []string) (string, string) {
|
||||
return key, strings.Join(values, ",")
|
||||
})
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func Source(sourceKey string, r *slog.Record) slog.Attr {
|
||||
fs := runtime.CallersFrames([]uintptr{r.PC})
|
||||
f, _ := fs.Next()
|
||||
var args []any
|
||||
if f.Function != "" {
|
||||
args = append(args, slog.String("function", f.Function))
|
||||
}
|
||||
if f.File != "" {
|
||||
args = append(args, slog.String("file", f.File))
|
||||
}
|
||||
if f.Line != 0 {
|
||||
args = append(args, slog.Int("line", f.Line))
|
||||
}
|
||||
|
||||
return slog.Group(sourceKey, args...)
|
||||
}
|
||||
|
||||
func StringSource(sourceKey string, r *slog.Record) slog.Attr {
|
||||
fs := runtime.CallersFrames([]uintptr{r.PC})
|
||||
f, _ := fs.Next()
|
||||
return slog.String(sourceKey, fmt.Sprintf("%s:%d (%s)", f.File, f.Line, f.Function))
|
||||
}
|
||||
|
||||
func FindAttribute(attrs []slog.Attr, groups []string, key string) (slog.Attr, bool) {
|
||||
// group traversal
|
||||
if len(groups) > 0 {
|
||||
for _, attr := range attrs {
|
||||
if attr.Value.Kind() == slog.KindGroup && attr.Key == groups[0] {
|
||||
attr, found := FindAttribute(attr.Value.Group(), groups[1:], key)
|
||||
if found {
|
||||
return attr, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return slog.Attr{}, false
|
||||
}
|
||||
|
||||
// starting here, groups is empty
|
||||
for _, attr := range attrs {
|
||||
if attr.Key == key {
|
||||
return attr, true
|
||||
}
|
||||
}
|
||||
|
||||
return slog.Attr{}, false
|
||||
}
|
||||
|
||||
func RemoveEmptyAttrs(attrs []slog.Attr) []slog.Attr {
|
||||
return lo.FlatMap(attrs, func(attr slog.Attr, _ int) []slog.Attr {
|
||||
if attr.Key != "" {
|
||||
if attr.Value.Kind() == slog.KindGroup {
|
||||
values := RemoveEmptyAttrs(attr.Value.Group())
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
attr.Value = slog.GroupValue(values...)
|
||||
}
|
||||
|
||||
if attr.Value.Equal(slog.Value{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []slog.Attr{attr}
|
||||
}
|
||||
|
||||
if attr.Value.Kind() != slog.KindGroup {
|
||||
return nil
|
||||
}
|
||||
|
||||
return RemoveEmptyAttrs(attr.Value.Group())
|
||||
})
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package slogcommon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func ContextExtractor(ctx context.Context, fns []func(ctx context.Context) []slog.Attr) []slog.Attr {
|
||||
attrs := []slog.Attr{}
|
||||
for _, fn := range fns {
|
||||
attrs = append(attrs, fn(ctx)...)
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
func ExtractFromContext(keys ...any) func(ctx context.Context) []slog.Attr {
|
||||
return func(ctx context.Context) []slog.Attr {
|
||||
attrs := make([]slog.Attr, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
attrs = append(attrs, slog.Any(key.(string), ctx.Value(key)))
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package slogcommon
|
||||
|
||||
import "log/slog"
|
||||
|
||||
func FindAttrByKey(attrs []slog.Attr, key string) (slog.Attr, bool) {
|
||||
for i := range attrs {
|
||||
if attrs[i].Key == key {
|
||||
return attrs[i], true
|
||||
}
|
||||
}
|
||||
|
||||
return slog.Attr{}, false
|
||||
}
|
||||
|
||||
func FindAttrByGroupAndKey(attrs []slog.Attr, groups []string, key string) (slog.Attr, bool) {
|
||||
if len(groups) == 0 {
|
||||
return FindAttrByKey(attrs, key)
|
||||
}
|
||||
|
||||
for i := range attrs {
|
||||
if attrs[i].Key == groups[0] && attrs[i].Value.Kind() == slog.KindGroup {
|
||||
attr, found := FindAttrByGroupAndKey(attrs[i].Value.Group(), groups[1:], key)
|
||||
if found {
|
||||
return attr, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return slog.Attr{}, false
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package slogcommon
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func AppendAttrsToGroup(groups []string, actualAttrs []slog.Attr, newAttrs ...slog.Attr) []slog.Attr {
|
||||
if len(groups) == 0 {
|
||||
actualAttrsCopy := make([]slog.Attr, 0, len(actualAttrs)+len(newAttrs))
|
||||
actualAttrsCopy = append(actualAttrsCopy, actualAttrs...)
|
||||
actualAttrsCopy = append(actualAttrsCopy, newAttrs...)
|
||||
return UniqAttrs(actualAttrsCopy)
|
||||
}
|
||||
|
||||
actualAttrs = slices.Clone(actualAttrs)
|
||||
|
||||
for i := range actualAttrs {
|
||||
attr := actualAttrs[i]
|
||||
if attr.Key == groups[0] && attr.Value.Kind() == slog.KindGroup {
|
||||
actualAttrs[i] = slog.Group(groups[0], lo.ToAnySlice(AppendAttrsToGroup(groups[1:], attr.Value.Group(), newAttrs...))...)
|
||||
return actualAttrs
|
||||
}
|
||||
}
|
||||
|
||||
return UniqAttrs(
|
||||
append(
|
||||
actualAttrs,
|
||||
slog.Group(
|
||||
groups[0],
|
||||
lo.ToAnySlice(AppendAttrsToGroup(groups[1:], []slog.Attr{}, newAttrs...))...,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// @TODO: should be recursive
|
||||
func UniqAttrs(attrs []slog.Attr) []slog.Attr {
|
||||
return uniqByLast(attrs, func(item slog.Attr) string {
|
||||
return item.Key
|
||||
})
|
||||
}
|
||||
|
||||
func uniqByLast[T any, U comparable](collection []T, iteratee func(item T) U) []T {
|
||||
result := make([]T, 0, len(collection))
|
||||
seen := make(map[U]int, len(collection))
|
||||
seenIndex := 0
|
||||
|
||||
for _, item := range collection {
|
||||
key := iteratee(item)
|
||||
|
||||
if index, ok := seen[key]; ok {
|
||||
result[index] = item
|
||||
continue
|
||||
}
|
||||
|
||||
seen[key] = seenIndex
|
||||
seenIndex++
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user