Skip to content

feat: add logs subcommand for mgrctl (Fixes #398)#724

Open
YatesMold wants to merge 6 commits into
uyuni-project:mainfrom
YatesMold:feat-mgrctl-logs
Open

feat: add logs subcommand for mgrctl (Fixes #398)#724
YatesMold wants to merge 6 commits into
uyuni-project:mainfrom
YatesMold:feat-mgrctl-logs

Conversation

@YatesMold
Copy link
Copy Markdown

What does this PR change?

This PR introduces the logs subcommand to mgrctl, allowing users to easily monitor and follow logs for various Uyuni services directly inside the container, without manually typing mgrctl exec "tail -f ...".

It implements the requested feature in Issue #398, adding support for the following flags:

  • --taskomatic: Shows Taskomatic logs (/var/log/rhn/rhn_tasko*.log)
  • --web: Shows Web UI logs (/var/log/rhn/rhn_web*.log)
  • --salt: Shows Salt logs (api, master, minion)
  • --reposync: Shows Reposync logs
  • --files: Regex support to filter specific reposync log files dynamically.
  • -f, --follow: Follow log output.

Codespace

Check if you already have a running container clicking on Running CodeSpace

Create CodeSpace About billing for Github Codespaces CodeSpace Billing Summary CodeSpace Limit

Test coverage

  • No tests: This subcommand acts as a wrapper that constructs file paths and re-uses the extensively tested exec.RunRawCmd logic to execute the tail commands.

  • DONE

Links

Issue(s): Fixes #398

  • DONE

Changelogs

  • No changelog needed
    (Note to reviewers: Happy to add a specific changelog entry if required for new subcommands!)

Signed-off-by: charvenetis <charvenetis@gmail.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new mgrctl logs subcommand to make it easier to tail/follow common Uyuni service logs from inside the server container, addressing Issue #398.

Changes:

  • Introduce mgrctl logs with service-selection flags (--taskomatic, --web, --salt, --reposync) plus --files filtering and -f/--follow.
  • Wire the new subcommand into the mgrctl root command.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
mgrctl/cmd/logs/logs.go Implements logs command: builds log path list and executes tail inside the container via podman/kubectl.
mgrctl/cmd/cmd.go Registers the new logs subcommand with the root CLI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mgrctl/cmd/logs/logs.go
Comment on lines +46 to +48
logsCmd.Flags().Bool("reposync", false, L("Show Reposync logs"))
logsCmd.Flags().String("files", "", L("Regex to filter reposync log files"))
logsCmd.Flags().BoolP("follow", "f", false, L("Follow log output"))
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

--files is silently ignored unless --reposync is also set, which is confusing given the flag help text. It would be better to validate this combination (e.g., return an error if --files is provided without --reposync).

Copilot uses AI. Check for mistakes.
Comment thread mgrctl/cmd/logs/logs.go Outdated
Comment on lines +110 to +111
// Re-use the execution logic from the exec package
return exec.RunRawCmd(command, commandArgs)
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

Unlike mgrctl exec, this command doesn’t preserve the exit status of the underlying podman/kubectl exec (errors bubble up to main, which exits with code 1). If tail fails (e.g., missing files), it would be useful to propagate the real exit code by handling *exec.ExitError similarly to mgrctl/cmd/exec/exec.go.

Copilot uses AI. Check for mistakes.
Comment thread mgrctl/cmd/logs/logs.go
Comment on lines +31 to +87
// NewCommand returns a new cobra.Command for logs.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
var flags flagpole

logsCmd := &cobra.Command{
Use: "logs",
Short: L("Show or follow logs of Uyuni services inside the container"),
RunE: func(cmd *cobra.Command, args []string) error {
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, run)
},
}

logsCmd.Flags().Bool("taskomatic", false, L("Show Taskomatic logs"))
logsCmd.Flags().Bool("web", false, L("Show Web UI logs"))
logsCmd.Flags().Bool("salt", false, L("Show Salt logs"))
logsCmd.Flags().Bool("reposync", false, L("Show Reposync logs"))
logsCmd.Flags().String("files", "", L("Regex to filter reposync log files"))
logsCmd.Flags().BoolP("follow", "f", false, L("Follow log output"))

utils.AddBackendFlag(logsCmd)
return logsCmd
}

func run(_ *types.GlobalFlags, flags *flagpole, _ *cobra.Command, _ []string) error {
cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter)
podName, err := cnx.GetPodName()
if err != nil {
return err
}

command, err := cnx.GetCommand()
if err != nil {
return err
}

var paths []string
if flags.Taskomatic {
paths = append(paths, "/var/log/rhn/rhn_tasko*.log")
}
if flags.Web {
paths = append(paths, "/var/log/rhn/rhn_web*.log")
}
if flags.Salt {
paths = append(paths, "/var/log/salt/api", "/var/log/salt/master", "/var/log/salt/minion")
}
if flags.Reposync {
if flags.Files != "" {
findCmd := fmt.Sprintf("find /var/log/rhn/reposync/ -type f | grep -E '%s'", flags.Files)
paths = append(paths, fmt.Sprintf("$(%s)", findCmd))
} else {
paths = append(paths, "/var/log/rhn/reposync/*.log")
}
}

if len(paths) == 0 {
return fmt.Errorf(L("please specify at least one service to get logs for (e.g., --web, --salt)"))
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

There are unit tests for other mgrctl subcommands (e.g., mgrctl/cmd/term/term_test.go, mgrctl/cmd/api/api_test.go), but this new command has none. Adding tests for flag validation (no service selected, --files without --reposync) and for the assembled exec arguments would help prevent regressions, especially around the reposync filtering behavior.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Indeed, unit tests would be needed. We at least want to be sure that the flags logic is correct.

Comment thread mgrctl/cmd/logs/logs.go Outdated
Comment on lines +78 to +80
findCmd := fmt.Sprintf("find /var/log/rhn/reposync/ -type f | grep -E '%s'", flags.Files)
paths = append(paths, fmt.Sprintf("$(%s)", findCmd))
} else {
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

--files is interpolated directly into a single-quoted grep -E inside a bash -c string. This breaks on patterns containing ' and also enables shell injection (e.g., closing the quote and appending commands) when running inside the container. Avoid building a shell pipeline from user input; pass the pattern as an argument (with proper escaping) or use find/grep options that don't require shell-eval of the pattern.

Copilot uses AI. Check for mistakes.
Comment thread mgrctl/cmd/logs/logs.go Outdated
Comment on lines +77 to +95
if flags.Files != "" {
findCmd := fmt.Sprintf("find /var/log/rhn/reposync/ -type f | grep -E '%s'", flags.Files)
paths = append(paths, fmt.Sprintf("$(%s)", findCmd))
} else {
paths = append(paths, "/var/log/rhn/reposync/*.log")
}
}

if len(paths) == 0 {
return fmt.Errorf(L("please specify at least one service to get logs for (e.g., --web, --salt)"))
}

tailCmd := "tail"
if flags.Follow {
tailCmd += " -f"
}

shCmd := fmt.Sprintf("%s %s", tailCmd, strings.Join(paths, " "))

Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

When --reposync --files yields no matches, the command substitution expands to an empty list and tail will read from stdin (and with -f it will appear to hang). Consider resolving the file list first and returning a clear error when it’s empty, instead of invoking tail with no path arguments.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@cbosdo cbosdo left a comment

Choose a reason for hiding this comment

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

This PR is a good start in the uyuni-tools code. See my comments to make it better.

Note that there is a goal to get all the logs files to the podman logs output for easier and more cloud-native access. This would force rewriting the implementation of this command.

Comment thread mgrctl/cmd/logs/logs.go
Comment on lines +31 to +87
// NewCommand returns a new cobra.Command for logs.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
var flags flagpole

logsCmd := &cobra.Command{
Use: "logs",
Short: L("Show or follow logs of Uyuni services inside the container"),
RunE: func(cmd *cobra.Command, args []string) error {
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, run)
},
}

logsCmd.Flags().Bool("taskomatic", false, L("Show Taskomatic logs"))
logsCmd.Flags().Bool("web", false, L("Show Web UI logs"))
logsCmd.Flags().Bool("salt", false, L("Show Salt logs"))
logsCmd.Flags().Bool("reposync", false, L("Show Reposync logs"))
logsCmd.Flags().String("files", "", L("Regex to filter reposync log files"))
logsCmd.Flags().BoolP("follow", "f", false, L("Follow log output"))

utils.AddBackendFlag(logsCmd)
return logsCmd
}

func run(_ *types.GlobalFlags, flags *flagpole, _ *cobra.Command, _ []string) error {
cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter)
podName, err := cnx.GetPodName()
if err != nil {
return err
}

command, err := cnx.GetCommand()
if err != nil {
return err
}

var paths []string
if flags.Taskomatic {
paths = append(paths, "/var/log/rhn/rhn_tasko*.log")
}
if flags.Web {
paths = append(paths, "/var/log/rhn/rhn_web*.log")
}
if flags.Salt {
paths = append(paths, "/var/log/salt/api", "/var/log/salt/master", "/var/log/salt/minion")
}
if flags.Reposync {
if flags.Files != "" {
findCmd := fmt.Sprintf("find /var/log/rhn/reposync/ -type f | grep -E '%s'", flags.Files)
paths = append(paths, fmt.Sprintf("$(%s)", findCmd))
} else {
paths = append(paths, "/var/log/rhn/reposync/*.log")
}
}

if len(paths) == 0 {
return fmt.Errorf(L("please specify at least one service to get logs for (e.g., --web, --salt)"))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Indeed, unit tests would be needed. We at least want to be sure that the flags logic is correct.

Comment thread mgrctl/cmd/logs/logs.go Outdated
logsCmd.Flags().Bool("web", false, L("Show Web UI logs"))
logsCmd.Flags().Bool("salt", false, L("Show Salt logs"))
logsCmd.Flags().Bool("reposync", false, L("Show Reposync logs"))
logsCmd.Flags().String("files", "", L("Regex to filter reposync log files"))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Explaining what the regexp is applied to would help the user to know what regex to write: does it need to match the whole path or only a segment? Those would be what a user could wonder.

Comment thread mgrctl/cmd/logs/logs.go Outdated
Use: "logs",
Short: L("Show or follow logs of Uyuni services inside the container"),
RunE: func(cmd *cobra.Command, args []string) error {
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, run)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

directly calling run makes it hard to unit test: use a variable with a function pointer as done in other commands (mostly in mgradm and mgrpxy)

Comment thread mgrctl/cmd/logs/logs.go Outdated
Comment on lines +84 to +89
if flags.Files != "" {
script := "files=$(find /var/log/rhn/reposync/ -type f | grep -E %q); " +
"if [ -z \"$files\" ]; then echo 'No matching reposync logs found.' >&2; exit 1; fi; " +
"echo $files"
findCmd := fmt.Sprintf(script, flags.Files)
paths = append(paths, fmt.Sprintf("$(%s)", findCmd))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We probably need to handle the case where the file doesn't exist yet but would show up later (relevant for the -f option if reposync hasn't been run on that repo yet)

Comment thread mgrctl/cmd/logs/logs.go Outdated
commandArgs = append(commandArgs, "bash", "-c", shCmd)

// Re-use the execution logic from the exec package
err = exec.RunRawCmd(command, commandArgs)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We have helpers to run commands in the shared module. Grep for NewRunner to find them.

Comment thread mgrctl/cmd/logs/logs.go Outdated
paths = append(paths, "/var/log/rhn/rhn_web*.log")
}
if flags.Salt {
paths = append(paths, "/var/log/salt/api", "/var/log/salt/master", "/var/log/salt/minion")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We don't run a Salt Minion inside the container, /var/log/salt/minion won't exist

@YatesMold YatesMold requested a review from cbosdo February 28, 2026 14:06
@YatesMold YatesMold force-pushed the feat-mgrctl-logs branch 2 times, most recently from 7608628 to 082c28c Compare March 4, 2026 15:09
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Mar 4, 2026

@YatesMold
Copy link
Copy Markdown
Author

Rebased on latest main, CI is green. Ready for review when you get a chance.

Copy link
Copy Markdown
Contributor

@cbosdo cbosdo left a comment

Choose a reason for hiding this comment

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

This issue needs more thinking before working on a PR: this command is clearly a band aid for logs that are not handled properly in the container.
This may be useful for users running the server on podman, but shouldn't be the way to push forward when running on Kubernetes. If we stick with a similar behavior, this needs to go to mgradm rather than mgrctl

@YatesMold YatesMold requested a review from cbosdo March 30, 2026 15:42
@YatesMold
Copy link
Copy Markdown
Author

Thanks for the clarification.
I'll leave it in your hands then. Happy to help if you need anything from my side

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add logs subcommand for mgrctl to better follow logs inside container

4 participants