Skip to content
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

Add plugin execution duration metric #5046

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

krissetto
Copy link
Contributor

@krissetto krissetto commented Apr 30, 2024

- What I did
Added utils for instrumenting plugin commands, and instrumented them to record their execution time.

Note: This is disconnected from the specific metrics and traces created by the plugins themselves and it not meant to replace those, it only represents a measurement of the binary execution of the plugin from the CLI's point of view

- How I did it

  • Added some utils
  • Added wrapper for exec.Cmd to be used for measuring the command's execution time
  • Wired up the utils so plugin execution metrics get sent

- How to verify it

- Description for the changelog

Instrument plugin execution with otel meter

- A picture of a cute animal (not mandatory but encouraged)

pinta

@krissetto krissetto changed the title Add plugin execution duration metric ⚠️ Add plugin execution duration metric ⚠️ Apr 30, 2024
@krissetto krissetto changed the title ⚠️ Add plugin execution duration metric ⚠️ 🚧 Add plugin execution duration metric 🚧 Apr 30, 2024
@codecov-commenter
Copy link

codecov-commenter commented Apr 30, 2024

Codecov Report

Attention: Patch coverage is 2.73973% with 71 lines in your changes missing coverage. Please review.

Project coverage is 61.16%. Comparing base (28c5652) to head (a5644cd).
Report is 456 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5046      +/-   ##
==========================================
- Coverage   61.33%   61.16%   -0.18%     
==========================================
  Files         298      298              
  Lines       20692    20750      +58     
==========================================
  Hits        12691    12691              
- Misses       7100     7158      +58     
  Partials      901      901              

@krissetto krissetto marked this pull request as ready for review May 9, 2024 12:04
@krissetto krissetto changed the title 🚧 Add plugin execution duration metric 🚧 Add plugin execution duration metric May 9, 2024
@krissetto
Copy link
Contributor Author

The first commit is from here, for the sake of testing

@krissetto krissetto force-pushed the plugin-duration-metric branch 2 times, most recently from 8e1b02d to b064920 Compare May 14, 2024 16:59
@vvoland
Copy link
Collaborator

vvoland commented May 15, 2024

@krissetto Please rebase :)

@krissetto
Copy link
Contributor Author

will fix this soon

@krissetto krissetto force-pushed the plugin-duration-metric branch 2 times, most recently from ba18362 to 57f59c8 Compare May 16, 2024 11:59
cli/command/telemetry_utils.go Show resolved Hide resolved
cli/command/telemetry_utils.go Outdated Show resolved Hide resolved
cli/command/telemetry_utils.go Outdated Show resolved Hide resolved
e2e/cli-plugins/run_test.go Outdated Show resolved Hide resolved
e2e/cli-plugins/run_test.go Outdated Show resolved Hide resolved
@krissetto krissetto force-pushed the plugin-duration-metric branch 3 times, most recently from d57c8fa to 8d7ff82 Compare May 16, 2024 15:51
if runErr != nil {
return nil, cobra.ShellCompDirectiveError
}
runErr = runCommand.Run()
if verifiedDockerCli, ok := dockerCli.(*command.DockerCli); ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just shadow dockerCli in this case instead of calling it verifiedDockerCli? Not a huge fan, sorry (I was wondering what made it "verified", and then realized it was just the typecast cli)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thing is if we shadow dockerCli here, then in the else condition we can't call dockerCli.Err() as it'd be a zero value and not the original value of dockerCli. I tried avoiding this where possible (see the other places where i do this cast), but here since we need the else i wasn't sure how to proceed if not renaming the var.

Maybe there are better ways?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a (can be un-exported) pluginInstrumenter interface and match that?


// wrappedCmd is used to wrap an exec.Cmd in order to instrument the
// command with otel by using the TimedRun() func
type wrappedCmd struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a wrapped exec.Cmd type, how hard would it be to just have pluginmanager.PluginRunCommand return a func that starts a timer, runs the command, and ends the timer?

Okay if we end up going with a wrapper, I just try hard to avoid those/introducing more state since now it has a pointer to a cli, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure that'd work..in the sense that we do things with the exec.Cmd that pluginmanager.PluginRunCommand returns aside from just running it (see the tryPluginRun() func as an example). So i wanted to keep the return value of InstrumentPluginCommand something close to what pluginmanager.PluginRunCommand currently returns. I guess we could rework all that but i'm not sure it'd be worth the extra work.

I'm not a fan of the wrapped type here either, but it seemed like a way to keep code changes relatively minimal on the usage side of things, and to allow code that doesn't need the otel bits to continue to function as usual. Happy to change the impl if we can find some simpler way to handle this. The pointer to DockerCli is really only needed to get the meter provider and the startPluginCommandTimer func atm.

The main idea of this impl was to try and keep the approach as close as possible to the one used by InstrumentCobraCommand

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. Let me take a closer look, but if this is indeed the least invasive way that's okay by me. I also don't love having to remember to call a new method TimedRun, which is why I'd have rather returned a func or something.

Signed-off-by: Christopher Petito <[email protected]>
@thaJeztah
Copy link
Member

needs a rebase @krissetto 🙈

Copy link
Member

@laurazard laurazard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM ✅

I had some nits/small comments, but I don't think they're blockers and might even be contentious, so I just pushed a branch here for comments.

Feel free to take them, I just wanted to simplify the code a bit, but we can merge this PR as-is and discuss other cleanups/refactors later.

Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I want to have a closer look at this PR to see if we can find some alternatives for some parts 😅

if runErr != nil {
return nil, cobra.ShellCompDirectiveError
}
runErr = runCommand.Run()
if verifiedDockerCli, ok := dockerCli.(*command.DockerCli); ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a (can be un-exported) pluginInstrumenter interface and match that?

Comment on lines +98 to +99
runCommand := verifiedDockerCli.InstrumentPluginCommand(pluginRunCommand)
runErr = runCommand.TimedRun()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InstrumentPluginCommand returns a non-exported type, and we always execute TimedRun() afterwards, except for the tryPluginRun function, where we construct it, and then later execute TimedRun.

It's unfortunate that exec.Cmd doesn't implement an interface, otherwise we could've made that the return, and we can't just define an interface that only has a func Run() error

Perhaps we should either make this function perform the TimedRun; puny-code;

if cli is a pluginInstrumenter {
    cli.RunInstrumented(pluginRunCommand)
} else {
    pluginRunCommand.Run()
}

But that could even be a function, because effectively it's not a method of the CLI;

func RunPluginCommandInstrumented(cli, pluginCommand)

Hmm.. but then we get to startPluginCommandTimer, which is even more generic, but missing the baseAttrs 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate that exec.Cmd doesn't implement an interface, otherwise we could've made that the return, and we can't just define an interface that only has a func Run() error

My thoughts exactly, it's kinda annoying.

We can surely find a better way to do this, my original goal with the first implementation was to keep the approach as close as possible to that used to instrument the cobra commands, but maybe that's not a great idea all things considered. The dockerCli struct / Cli interface situation in the CLI (since we don't always pass around just the interface or just the struct) also caused some minor headaches and ugliness that would be nice to avoid if possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants