Skip to content

Commit 534c489

Browse files
authored
Merge pull request #95 from thin-edge/feat-container-logs-command
feat: add internal command to get container logs
2 parents bda86b5 + ff91d48 commit 534c489

File tree

4 files changed

+158
-0
lines changed

4 files changed

+158
-0
lines changed

cli/tools/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func NewToolsCommand(cmdCli cli.Cli) *cobra.Command {
1414
}
1515
cmd.AddCommand(
1616
NewContainerCloneCommand(cmdCli),
17+
NewContainerLogsCommand(cmdCli),
1718
)
1819
return cmd
1920
}

cli/tools/container_logs.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
Copyright © 2024 thin-edge.io <[email protected]>
3+
*/
4+
package tools
5+
6+
import (
7+
"context"
8+
"errors"
9+
"log/slog"
10+
"strings"
11+
12+
"github.com/spf13/cobra"
13+
"github.com/thin-edge/tedge-container-plugin/pkg/cli"
14+
"github.com/thin-edge/tedge-container-plugin/pkg/container"
15+
)
16+
17+
type ContainerLogsCommand struct {
18+
*cobra.Command
19+
20+
CommandContext cli.Cli
21+
22+
// Options
23+
Tail string
24+
Since string
25+
Until string
26+
Timestamps bool
27+
Follow bool
28+
Details bool
29+
}
30+
31+
// NewContainerLogsCommand creates a new container logs command
32+
func NewContainerLogsCommand(ctx cli.Cli) *cobra.Command {
33+
command := &ContainerLogsCommand{
34+
CommandContext: ctx,
35+
}
36+
cmd := &cobra.Command{
37+
Use: "container-logs [OPTIONS] CONTAINER",
38+
Short: "Fetch the logs of a container",
39+
RunE: command.RunE,
40+
Args: cobra.ArbitraryArgs,
41+
SilenceUsage: true,
42+
}
43+
44+
cmd.Flags().StringVar(&command.Since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)")
45+
cmd.Flags().StringVar(&command.Until, "until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)")
46+
cmd.Flags().StringVarP(&command.Tail, "tail", "n", "all", "Number of lines to show from the end of the logs")
47+
cmd.Flags().BoolVarP(&command.Follow, "follow", "f", false, "Follow log output")
48+
cmd.Flags().BoolVarP(&command.Timestamps, "timestamps", "t", false, "Show timestamps")
49+
cmd.Flags().BoolVar(&command.Details, "details", false, "Show extra details provided to logs")
50+
command.Command = cmd
51+
return cmd
52+
}
53+
54+
func (c *ContainerLogsCommand) RunE(cmd *cobra.Command, args []string) error {
55+
slog.Debug("Executing", "cmd", cmd.CalledAs(), "args", args)
56+
57+
containerCli, err := container.NewContainerClient()
58+
if err != nil {
59+
return err
60+
}
61+
62+
ctx := context.Background()
63+
64+
if len(args) == 0 {
65+
slog.Info("Fetching logs for current container (if running inside a container)")
66+
con, err := containerCli.Self(ctx)
67+
if err != nil {
68+
return err
69+
}
70+
args = append(args, con.ID)
71+
}
72+
73+
// Write container logs (stdout and stderr) to stdout
74+
out := cmd.OutOrStdout()
75+
errs := make([]error, 0)
76+
for _, con := range args {
77+
idOrName := strings.TrimPrefix(con, "/")
78+
slog.Info("Fetching logs.", "id", idOrName)
79+
if err := containerCli.ContainerLogs(ctx, out, idOrName, container.LogsOptions{
80+
ShowStdout: true,
81+
ShowStderr: true,
82+
Since: c.Since,
83+
Until: c.Until,
84+
Timestamps: c.Timestamps,
85+
Follow: c.Follow,
86+
Tail: c.Tail,
87+
Details: c.Details,
88+
}); err != nil {
89+
errs = append(errs, err)
90+
}
91+
}
92+
93+
return errors.Join(errs...)
94+
}

pkg/container/container.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,22 @@ func (c *ContainerClient) UpdateRequired(ctx context.Context, containerID string
924924
return true, prevContainer, nil
925925
}
926926

927+
// Log options type alias
928+
type LogsOptions container.LogsOptions
929+
930+
func (c *ContainerClient) ContainerLogs(ctx context.Context, w io.Writer, containerID string, opts LogsOptions) error {
931+
reader, logErr := c.Client.ContainerLogs(ctx, containerID, container.LogsOptions(opts))
932+
if logErr != nil {
933+
return logErr
934+
}
935+
defer reader.Close()
936+
_, err := StdCopy(w, w, reader)
937+
if err != nil {
938+
return err
939+
}
940+
return nil
941+
}
942+
927943
type CloneOptions struct {
928944
Image string
929945
HealthyAfter time.Duration

tests/container-logs.robot

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
*** Settings ***
2+
Resource ./resources/common.robot
3+
Library String
4+
Library Cumulocity
5+
Library DeviceLibrary bootstrap_script=bootstrap.sh
6+
7+
Suite Setup Suite Setup
8+
9+
Test Tags docker podman
10+
11+
*** Test Cases ***
12+
13+
Get Container Logs
14+
# Run dummy container then exit
15+
DeviceLibrary.Execute Command cmd=tedge-container engine docker run --name app10 httpd:2.4.61-alpine sh -c 'echo hello inside container stdout; echo hello inside container stderr >&2;'
16+
17+
# Fetch logs
18+
${output}= DeviceLibrary.Execute Command sudo tedge-container tools container-logs app10
19+
Should Contain ${output} hello inside container stdout
20+
Should Contain ${output} hello inside container stderr
21+
22+
Get Container Logs with only last N lines
23+
# Run dummy container then exit
24+
DeviceLibrary.Execute Command cmd=tedge-container engine docker run --name app11 httpd:2.4.61-alpine sh -c 'echo hello inside container stdout; echo hello inside container stderr >&2;'
25+
26+
# Fetch logs
27+
${output}= DeviceLibrary.Execute Command sudo tedge-container tools container-logs app11 -n 1
28+
29+
${total_lines}= String.Get Line Count ${output}
30+
Should Be Equal As Integers ${total_lines} 1
31+
32+
Should Not Contain ${output} hello inside container stdout
33+
Should Contain ${output} hello inside container stderr
34+
35+
*** Keywords ***
36+
37+
Suite Setup
38+
${DEVICE_SN}= Setup
39+
Set Suite Variable $DEVICE_SN
40+
Cumulocity.External Identity Should Exist ${DEVICE_SN}
41+
Cumulocity.Should Have Services name=tedge-container-plugin service_type=service min_count=1 max_count=1 timeout=30
42+
43+
${operation}= Cumulocity.Execute Shell Command sudo tedge-container engine docker network create tedge ||:
44+
Operation Should Be SUCCESSFUL ${operation} timeout=60
45+
46+
# Create data directory
47+
DeviceLibrary.Execute Command mkdir /data

0 commit comments

Comments
 (0)