-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
feat: add query-log-path
configuration
#25710
base: master-1.x
Are you sure you want to change the base?
Conversation
a440193
to
410241e
Compare
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.
Did not look at the tests
86d496e
to
a4a995f
Compare
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.
Quick review (didn't look at tests) on this, treating it like a draft PR, per your request.
query/executor.go
Outdated
// WatcherInterface is used for any file watch functionality using fsnotify | ||
type WatcherInterface interface { | ||
Event() chan fsnotify.Event | ||
Error() chan error |
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.
Is there a direction on this channel?
query/executor.go
Outdated
e.Logger.Debug("received file event", zap.String("event", event.Name)) | ||
|
||
if event.Op == fsnotify.Remove || event.Op == fsnotify.Rename { | ||
e.mu.Lock() |
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.
perhaps toss this block into a lambda and use defer mu.Unlock()
? That protects against future additional return paths.
query/executor.go
Outdated
case err, ok := <-w.Error(): | ||
closeErr := w.Close() | ||
if closeErr != nil { | ||
return fmt.Errorf("failed to close watcher: %w", closeErr) |
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 don't want to throw away the first error. Use errors.Join
to get the original error and the Close
error.
query/file_log_watcher.go
Outdated
return f.fsWatcher.Events | ||
} | ||
|
||
func (f *FileLogWatcher) Error() chan error { |
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.
channel direction?
query/file_log_watcher.go
Outdated
logFile, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) | ||
if err != nil { | ||
f.logger.Error("failed to open log file", zap.Error(err), zap.String("path", f.path)) | ||
return 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.
return err
??
query/file_log_watcher.go
Outdated
existingCore := f.logger.Core() | ||
encoder, err := l.NewEncoder(f.formatterConfig) | ||
if err != nil { | ||
return err |
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.
Do we want to Close
the file here? If this isn't a fatal error for the database we should probably close the file to avoid leaking the handle.
query/file_log_watcher.go
Outdated
f.logger = zap.New(newCore) | ||
|
||
if err := f.fsWatcher.Add(f.path); err != nil { | ||
return err |
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 as above. What needs to be cleaned up here, if anything?
} | ||
|
||
func (f *FileLogWatcher) Close() error { | ||
if err := f.currFile.Sync(); err != 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.
- Do we need mutex protection here?
- We should always close the file, even if the sync fails.
- We should close the
fsWatcher
even if the sync or close fails errors.Join
is your friend.
query/executor_test.go
Outdated
|
||
for time.Now().Before(deadline) { | ||
if mockWatcher.ChangeEvents.Load() == 1 { | ||
break |
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.
Are you breaking here, or running out the deadline?
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.
Some changes for debugging the CI problem, some general changes
query/executor_test.go
Outdated
require.Equal(t, uint64(1), mockWatcher.ChangeEvents.Load()) | ||
require.Equal(t, uint64(1), mockWatcher.RemovedPaths.Load()) | ||
|
||
err = mockWatcher.Close() |
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 already in the defer
above. Move the require.NoError
into a lambda for the defer
.
query/executor_test.go
Outdated
require.Equal(t, uint64(1), mockWatcher.ChangeEvents.Load()) | ||
require.Equal(t, uint64(1), mockWatcher.RemovedPaths.Load()) | ||
|
||
err = mockWatcher.Close() |
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.
As above, the Close
is already in the defer
. Move the require.NoError
into a lambda in the defer
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.
Oops yeah meant to adjust that. Going to move to the lambdas as per your comment
query/executor_test.go
Outdated
Op: fsnotify.Remove, | ||
} | ||
|
||
for time.Now().Before(deadline) { |
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.
Perhaps use time.After
instead of a loop?
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 call I'll try that out
query/executor_test.go
Outdated
|
||
for time.Now().Before(deadline) { | ||
if mockWatcher.ChangeEvents.Load() == 1 { | ||
break |
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.
Set a bool variable here to see if this is the loop exit or the ChangeEvents.Load
is
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 PR adds the ability for an admin to define 'query-log-path' as a configuration item field. A new configuration option is created ```toml [data] query-log-path = "/var/influx/query.log" ``` This will enable query logging being written to a log file on disk. Logged queries example: ``` {"level":"info","ts":1735248393.084461,"msg":"Executing query","query":"SHOW DATABASES"} {"level":"info","ts":1735248395.092188,"msg":"Executing query","query":"SHOW DATABASES"} {"level":"info","ts":1735248398.58039,"msg":"Executing query","query":"SELECT * FROM stress.autogen.m0 LIMIT 20"} ```
2109d11
to
efcf13c
Compare
// FileWatcher is not meant to be used with interactive shell sessions so | ||
// console as a logging format will not be supported. | ||
if format == "console" { | ||
logger.Error("unknown logging format", zap.String("format", format), zap.String("path", path)) |
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.
console
isn't an unknown logging format. It's an unsupported format for query logging.
format = "logfmt" | ||
} | ||
|
||
logFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) |
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.
Generally it's better to use a permission of 0666
and let the system's umask value get the permissions the admin really wants (source: lots of headaches). It's likely the admin might want the file to be group writable for the external log rotation to work properly. Hard coding 0644
here might cause issues with said log rotation.
@@ -487,6 +487,16 @@ func (s *Server) Open() error { | |||
// so it only logs as is appropriate. | |||
s.QueryExecutor.TaskManager.Logger = s.Logger | |||
} | |||
if s.config.Data.QueryLogPath != "" { | |||
path := s.config.Data.QueryLogPath | |||
flw := query.NewFileLogWatcher(s.QueryExecutor, path, s.Logger, s.config.Logging.Format) |
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.
Either query.NewFileLogWatcher
is misnamed, or QueryExecutor.WithLogWriter
is misnamed. It looks like the wrong thing is getting passed around.
} | ||
} | ||
|
||
func (e *Executor) WithLogWriter(w WatcherInterface, ctx context.Context) { |
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.
ctx
should be the first parameter. From the docs:
The Context should be the first parameter, typically named ctx.
// FileWatcher is not meant to be used with interactive shell sessions so | ||
// console as a logging format will not be supported. | ||
if format == "console" { | ||
logger.Error("unknown logging format", zap.String("format", format), zap.String("path", path)) |
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 feel like it would be cleaner for NewFileLogWatcher
to return an additional error value and not take the logger object in. The caller should log the error returned.
Stylistically, a function that on error only returns nil
feels like it should be left in the world of C.
if flw != nil { | ||
s.QueryExecutor.WithLogWriter(flw, context.Background()) | ||
} else { | ||
s.Logger.Error("error creating log writer", zap.String("path", path)) |
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.
Yeah, if NewFileLogWatcher
returned an error, you could log the error right here instead of this generic log message.
@@ -65,7 +65,7 @@ func newEncoder(format string) (zapcore.Encoder, error) { | |||
} | |||
} | |||
|
|||
func newEncoderConfig() zapcore.EncoderConfig { | |||
func NewEncoderConfig() zapcore.EncoderConfig { |
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 can't find any uses of NewEncodingConfig
outside of the logger
package. Is there a reason to change it to public?
@@ -289,6 +306,94 @@ func (e *Executor) WithLogger(log *zap.Logger) { | |||
e.TaskManager.Logger = e.Logger | |||
} | |||
|
|||
func startFileLogWatcher(w WatcherInterface, e *Executor, ctx context.Context) error { |
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.
startFileLogWatcher
made me think this was going to spawn a goroutine, but it doesn't. It's the body of a goroutine. Maybe either doFileLogWatch
or simply fileLogWatch
?
@@ -242,6 +257,8 @@ type Executor struct { | |||
|
|||
// expvar-based stats. | |||
stats *Statistics | |||
|
|||
mu sync.Mutex |
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 would not put mu
at the bottom of the struct. A developer going into this code might miss mu
and not lock appropriately.
My preference is to put mu
above all the members that it will protect, along with a comment that mu protects all following members
. Members that don't require protection can go above mu
.
@devanbenz The hardest part of this, IMO, is correctly handling the fsnotify events. It would be great to have some tests that actually exercise that functionality. |
@devanbenz You might even consider just using the real thing for all the tests instead of the mocked interface. Use |
@devanbenz The more I think about it, the more I like getting rid of the mock. I'm interested in how the
|
This makes sense to me. I'm usually under the mindset to not "test" third party libraries but I could foresee this being an exception to that since its correctness is core to the changes. Do you think I should get rid of the interface and just make everything concrete as well? |
flw := query.NewFileLogWatcher(s.QueryExecutor, path, s.Logger, s.config.Logging.Format) | ||
if flw != nil { | ||
s.QueryExecutor.WithLogWriter(flw, context.Background()) |
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.
@devan I had an idea last night on how to make this way more flexible and reusable.
First, change QueryExecutor.WithLogWriter(*FileLogWatcher)
to QueryExecutor.WithQueryLogger(*zap.Logger)
. By taking a *zap.Logger
, we have way more flexibility in how queries can be logged. It will also decouple log rotation logic from the QueryExecutor
if the code ends up being split between OSS and Enterprise.
Second, change query.NewFileLogWatcher(...) *FileLogWatcher
to query.NewRotatableLogger(...) (*zap.Logger, error)
(or logger.NewRotatableLogger...
). This logger can then be used anywhere we need a log file that can be rotated by an external log rotator without making changes to the client code using the logger. Note: The goroutine to watch the signal channel will keep the references to the required data so it won't be garbage collected.
Adding this comment from slack for posterity:
|
This PR adds the ability for an admin to define 'query-log-path' as a configuration item field.
A new configuration option is created
This will enable query logging.
Logged queries example: