-
Notifications
You must be signed in to change notification settings - Fork 7.5k
fix: reinstate logger parameter to actions package #31411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Fixes: helm#31399 Signed-off-by: Evans Mungai <mbuevans@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
benoittgt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot @banjoh for the work on this! I have few questions
| d.SetLogger(cfg.Logger()) | ||
| d.SetNamespace(namespace) | ||
| store = storage.Init(d) | ||
| case "sql": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also set the logger for SQL?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
pkg/kube/client.go
Outdated
|
|
||
| // logger returns the logger for the Client. If nil, returns slog.Default(). | ||
| func (c *Client) Logger() *slog.Logger { | ||
| if lg := c.logger.Load(); lg != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To reduce duplication what about having something like ?
// internal/logging/logger.go
package logging
import "log/slog"
func GetLogger(logger *slog.Logger) *slog.Logger {
[...]
}
func SetLogger(newLogger *slog.Logger) *slog.Logger {
[...]
}or maybe go even further if we accept this (also with the nil as slog.Default()).
type Holder struct {
logger *slog.Logger
}
func (h *Holder) Logger() *slog.Logger {
if h.logger != nil {
return h.logger
}
return slog.Default()
}
func (h *Holder) SetLogger(newLogger *slog.Logger) {
if newLogger == nil {
h.logger = slog.Default()
return
}
h.logger = newLogger
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion. I assume you are suggesting using Holder as a struct embedding, right? Like so
type MyThing struct {
Holder
}
func (m *MyThing) Do() {
m.Logger().Info("Doing something")
}
pkg/kube/client.go
Outdated
|
|
||
| func (c *Client) SetLogger(newLogger *slog.Logger) { | ||
| if newLogger == nil { | ||
| c.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as before. What about using slog.Default() when the logger is nil, and requiring users to explicitly pass something like slog.New(slog.NewJSONHandler(io.Discard, nil)) if they want to discard logs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had that in the begin then I changed my mind. When consuming an API like this, I find passing nil i.e the default value of a pointer is synonymous to "unset my value". The effect of "unsetting" a logger is not to log anything.
pkg/action/action.go
Outdated
| HookOutputFunc func(namespace, pod, container string) io.Writer | ||
|
|
||
| // logger is an slog.Logger pointer to use with the Configuration instance | ||
| logger atomic.Pointer[slog.Logger] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious, why are we not using a *slog.Logger directly here? If I use the SDK example and adapt it with your code, I don't see the need to use and atomic pointer.
func runUpgrade(....) error {
upgradeLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).With(
slog.String("request_id", "abc-123"),
)
actionConfig := action.NewConfiguration()
actionConfig.SetLogger(upgradeLogger)
// [...]
upgradeClient := action.NewUpgrade(actionConfig) // then I have my logger per action 🤔 Maybe I’m missing some scope detail or other considerations you had in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to make sure SetLogger and Logger are thread safe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m wondering in which scenario someone would actually modify the logger during execution. 🤔
If we typically set it once in Configuration (for example, at action initialization), I don’t see a case where concurrent reads and writes would happen:
cfg := action.NewConfiguration()
// Goroutine 1: Constantly changing logger
go func() {
for {
cfg.SetLogger(logger1) // Writing
}
}()
// Goroutine 2: Reading logger
go func() {
for {
cfg.Logger().Info("test") // Reading
}
}()Wouldn’t this kind of pattern be unusual in practice? It feels like we’d only set the logger once and reuse it across actions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn’t this kind of pattern be unusual in practice? It feels like we’d only set the logger once and reuse it across actions.
Its unusual to do this for sure, but given that the cost of implementing thread safety is low, why not ensure its there? If performance is a concern, using atomic.Pointer ensure we have lock-free thread safety so the cost is negligible.
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
pkg/action/action.go
Outdated
| mutex sync.Mutex | ||
| } | ||
|
|
||
| func NewConfiguration() *Configuration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| func NewConfiguration() *Configuration { | |
| func NewConfiguration(options...ConfigurationOption) *Configuration { |
Option parameters might be better than this SetLogger() pattern?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ie.
type ConfigurationOption func(c *Configuration)
// Override the default logging handler
// If unspecified, the default logger will be used
func ConfigurationSetLogger(h *slog.Handler) ConfigurationOption {
return func(c *Configuration) {
c.logger = slog.New(h)
}
}
func NewConfiguration(options...ConfigurationOption) {
c := &Configuration{}
for _, o := options {
o(c)
}
if o.logger == nil {
o.logger = slog.Default()
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would assume that the user can set the logger when creating the action Configuration
(and doesn't want to change it later; but would be a somewhat "more idiomatic" or "cleaner" approach, perhaps)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. It allows us to add more options in future
Co-authored-by: George Jenkins <gvjenkins@gmail.com> Signed-off-by: Evans Mungai <mbuevans@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
robertsirc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not to be difficult, but shouldn't we put some test around this?
We could. They should be trivial to add |
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
@robertsirc I've added some tests. Please take a look. Thanks |
| for _, o := range options { | ||
| o(c) | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need the if c.logger == nil { c.SetLogger(slog.Default()) } here?
Otherwise nothing will se tthe logger, and Logger() will default to the discard handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yes. Good catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. logger would never be nil since it's not a pointer type
type LogHolder struct {
logger atomic.Pointer[slog.Logger]
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The atomic value/pointer is nil:
if c.logger.Load() == nil { c.SetLogger(slog.Default()) }
Signed-off-by: Evans Mungai <mbuevans@gmail.com>
|
Hello TL;DR. Looks good on SDK and cli. I did a full test. First in SDK scenario. // Inspired by https://github.com/helm/helm-www/blob/main/sdkexamples/upgrade.go
// main.go
package main
import (
"context"
"log/slog"
"os"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/chart/loader"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/kube"
releasev1 "helm.sh/helm/v4/pkg/release/v1"
)
func main() {
baseLogger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
logger := baseLogger.With(
slog.String("deployment-id", "12345"),
)
logger.Info("Starting Helm upgrade test with new slog integration")
settings := cli.New()
settings.Debug = true
actionConfig := action.NewConfiguration()
actionConfig.SetLogger(logger.Handler())
if err := actionConfig.Init(
settings.RESTClientGetter(),
settings.Namespace(),
os.Getenv("HELM_DRIVER"),
); err != nil {
logger.Error("Failed to initialize action config", "error", err)
os.Exit(1)
}
upgradeClient := action.NewUpgrade(actionConfig)
// release type will be *releasev1.Release"
upgradeClient.WaitStrategy = kube.HookOnlyStrategy
releaseName := "simple-test"
chartPath := "../simple-test"
logger.Info("Upgrading release", "release", releaseName, "chart", chartPath)
chart, err := loader.Load(chartPath)
if err != nil {
logger.Error("Failed to load chart", "error", err)
os.Exit(1)
}
rel, err := upgradeClient.RunWithContext(context.Background(), releaseName, chart, nil)
if err != nil {
logger.Error("Upgrade failed", "error", err)
os.Exit(1)
}
r := rel.(*releasev1.Release)
logger.Info("Upgrade successful", "release", r.Name, "version", r.Version, "status", r.Info.Status.String())
}go run main.go
[...]
time=2025-10-24T18:05:23.280+02:00 level=DEBUG msg="Patched resource" deployment-id=12345 namespace=default name=simple-test gvk="apps/v1, Kind=Deployment"
time=2025-10-24T18:05:23.297+02:00 level=DEBUG msg="updating status for upgraded release" deployment-id=12345 name=simple-test
time=2025-10-24T18:05:23.300+02:00 level=INFO msg="Upgrade successful" deployment-id=12345 release=simple-test version=21 status=deployedWIth cli It looks good. |
While looking at SDK feature for v4. I was surprise with the error: > "reporter failed to start: event funnel closed: context deadline exceeded" It is related when you forgot to set a minimal timeout: ```go upgradeClient := action.NewUpgrade(actionConfig) upgradeClient.WaitStrategy = kube.StatusWatcherStrategy // When Timeout is zero, the status wait uses a context with zero timeout which // immediately expires causing "context deadline exceeded" errors. upgradeClient.Timeout = 2 * time.Minute ``` Also maybe it should be even more documented. Initial [PR](helm#13604) say: > I have not written any docs, I assume that can be done when we are closer to Helm 4, a lot of it is covered by linking to - https://github.com/kubernetes-sigs/cli-utils/blob/master/pkg/kstatus/README.md Related: - helm#31411 (comment)
> "reporter failed to start: event funnel closed: context deadline exceeded" It happens when you forget to set a minimal timeout: ```go upgradeClient := action.NewUpgrade(actionConfig) upgradeClient.WaitStrategy = kube.StatusWatcherStrategy // When Timeout is zero, the status wait uses a context with zero timeout which // immediately expires causing "context deadline exceeded" errors. upgradeClient.Timeout = 2 * time.Minute ``` Also maybe it might be worth documenting this more clearly. Initial [PR](helm#13604) say: > I have not written any docs, I assume that can be done when we are closer to Helm 4, a lot of it is covered by linking to - https://github.com/kubernetes-sigs/cli-utils/blob/master/pkg/kstatus/README.md Related: - helm#31411 (comment) Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
> "reporter failed to start: event funnel closed: context deadline exceeded" It happens when you forget to set a minimal timeout: ```go upgradeClient := action.NewUpgrade(actionConfig) upgradeClient.WaitStrategy = kube.StatusWatcherStrategy // When Timeout is zero, the status wait uses a context with zero timeout which // immediately expires causing "context deadline exceeded" errors. upgradeClient.Timeout = 2 * time.Minute ``` Also maybe it might be worth documenting this more clearly. Initial [PR](helm#13604) say: > I have not written any docs, I assume that can be done when we are closer to Helm 4, a lot of it is covered by linking to - https://github.com/kubernetes-sigs/cli-utils/blob/master/pkg/kstatus/README.md Related: - helm#31411 (comment) Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
mattfarina
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
I know there is another PR to expand on test, too.
robertsirc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
|
Hi, I'm gonna do a review on behalf of Flux later today |
|
LGTM! |
scottrigby
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm! Was also wanting to hear from Flux review - thanks @matheuscscp 👍
TerryHowe
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/lgtm
Thanks for working on this, nice work!
|
|
||
| // Logger returns the logger for the LogHolder. If nil, returns slog.Default(). | ||
| func (l *LogHolder) Logger() *slog.Logger { | ||
| if lg := l.logger.Load(); lg != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice if this would default to slog.Default().Handler() so this doesn't have to call SetLogger all over the place. There could be a follow up to do this if anyone else thinks this is a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this does sound useful as a follow-up IMO
> "reporter failed to start: event funnel closed: context deadline exceeded" It happens when you forget to set a minimal timeout: ```go upgradeClient := action.NewUpgrade(actionConfig) upgradeClient.WaitStrategy = kube.StatusWatcherStrategy // When Timeout is zero, the status wait uses a context with zero timeout which // immediately expires causing "context deadline exceeded" errors. upgradeClient.Timeout = 2 * time.Minute ``` Also maybe it might be worth documenting this more clearly. Initial [PR](helm#13604) say: > I have not written any docs, I assume that can be done when we are closer to Helm 4, a lot of it is covered by linking to - https://github.com/kubernetes-sigs/cli-utils/blob/master/pkg/kstatus/README.md Related: - helm#31411 (comment) Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
| } | ||
|
|
||
| // LoggerSetterGetter is an interface that can set and get a logger | ||
| type LoggerSetterGetter interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@banjoh There also used to be a method for passing a logger to the pkg/storage.Storage. Now the way you can do it is by passing a driver.Driver to pkg/storage.Init() that implements this LoggerSetterGetter interface. Can this interface be exposed, please? 🙏
| // Get logger from driver if it implements the LoggerSetterGetter interface | ||
| if ls, ok := d.(logging.LoggerSetterGetter); ok { | ||
| ls.SetLogger(s.Logger().Handler()) | ||
| } else { | ||
| // If the driver does not implement the LoggerSetterGetter interface, set the default logger | ||
| s.SetLogger(slog.Default().Handler()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, a bit too late, but there's a bug here. Line 344 should be s.SetLogger()
What this PR does / why we need it:
Add ability to override logger when creating new actions
Here is an example of how to use it
Fixes: #31399
Special notes for your reviewer:
SetLogger()method instead of passing the logger inaction.Configuration.Init()function, or setting it viaaction.Configuration.Logslog.Loggerinstead of the olderloglogger library in v3If applicable:
docs neededlabel should be applied if so)