diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index de258d29d..1283a6188 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -2575,22 +2575,37 @@ On first run, creates the file with all settings commented out at their defaults
Signal to send (default: {})
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+ Show logs since timestamp (e.g. unix timestamp)
+
Current or existing image reference in the image-name[:tag] format
Tag for the built image
+
+ Number of lines to show from the end of the logs
+
New image reference in the image-name[:tag] format
Time in seconds to wait before executing (default 5)
+
+ Show timestamps in log output
+
Open a TTY with the container process.
{Locked="TTY"}Command line arguments should not be translated
+
+ Type of the object to inspect
+
+
+ Show logs before timestamp (e.g. unix timestamp)
+
Output verbose details
@@ -2764,9 +2779,6 @@ On first run, creates the file with all settings commented out at their defaults
Name or Id of any object type
-
- Type of the object to inspect
-
Inspect objects.
diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h
index a7bfe4e9b..dda890786 100644
--- a/src/windows/wslc/arguments/ArgumentDefinitions.h
+++ b/src/windows/wslc/arguments/ArgumentDefinitions.h
@@ -87,13 +87,17 @@ _(Session, "session", NO_ALIAS, Kind::Value, L
_(SessionId, "session-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SessionIdPositionalArgDescription()) \
_(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L"Path to the session storage directory") \
_(Signal, "signal", L"s", Kind::Value, Localization::WSLCCLI_SignalArgDescription(L"SIGKILL")) \
+_(Since, "since", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SinceArgDescription()) \
_(Source, "source", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SourceArgDescription()) \
_(Tag, "tag", L"t", Kind::Value, Localization::WSLCCLI_TagArgDescription()) \
+_(Tail, "tail", L"n", Kind::Value, Localization::WSLCCLI_TailArgDescription()) \
_(Target, "target", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_TargetArgDescription()) \
_(Time, "time", L"t", Kind::Value, Localization::WSLCCLI_TimeArgDescription()) \
+_(Timestamps, "timestamps", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_TimestampsArgDescription()) \
_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, Localization::WSLCCLI_TMPFSArgDescription()) \
_(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \
_(Type, "type", L"t", Kind::Value, Localization::WSLCCLI_TypeArgDescription()) \
+_(Until, "until", NO_ALIAS, Kind::Value, Localization::WSLCCLI_UntilArgDescription()) \
_(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription()) \
_(Username, "username", L"u", Kind::Value, Localization::WSLCCLI_LoginUsernameArgDescription()) \
_(Verbose, "verbose", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \
diff --git a/src/windows/wslc/commands/ContainerLogsCommand.cpp b/src/windows/wslc/commands/ContainerLogsCommand.cpp
index da4796f30..350a0f508 100644
--- a/src/windows/wslc/commands/ContainerLogsCommand.cpp
+++ b/src/windows/wslc/commands/ContainerLogsCommand.cpp
@@ -30,6 +30,10 @@ std::vector ContainerLogsCommand::GetArguments() const
Argument::Create(ArgType::ContainerId, true),
Argument::Create(ArgType::Session),
Argument::Create(ArgType::Follow),
+ Argument::Create(ArgType::Tail),
+ Argument::Create(ArgType::Since),
+ Argument::Create(ArgType::Until),
+ Argument::Create(ArgType::Timestamps),
};
}
diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp
index 90af89e87..b281f2a0d 100644
--- a/src/windows/wslc/services/ContainerService.cpp
+++ b/src/windows/wslc/services/ContainerService.cpp
@@ -467,7 +467,7 @@ InspectContainer ContainerService::Inspect(Session& session, const std::string&
return wsl::shared::FromJson(output.get());
}
-void ContainerService::Logs(Session& session, const std::string& id, bool follow)
+void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail)
{
wil::com_ptr container;
THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
@@ -476,8 +476,9 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow
COMOutputHandle stderrHandle;
WSLCLogsFlags flags = WSLCLogsFlagsNone;
WI_SetFlagIf(flags, WSLCLogsFlagsFollow, follow);
+ WI_SetFlagIf(flags, WSLCLogsFlagsTimestamps, timestamps);
- THROW_IF_FAILED(container->Logs(flags, &stdoutHandle, &stderrHandle, 0, 0, 0));
+ THROW_IF_FAILED(container->Logs(flags, &stdoutHandle, &stderrHandle, since, until, tail));
wsl::windows::common::relay::MultiHandleWait io;
io.AddHandle(std::make_unique>(
diff --git a/src/windows/wslc/services/ContainerService.h b/src/windows/wslc/services/ContainerService.h
index aeddaff9f..6292b7ab6 100644
--- a/src/windows/wslc/services/ContainerService.h
+++ b/src/windows/wslc/services/ContainerService.h
@@ -32,6 +32,6 @@ struct ContainerService
static std::vector List(models::Session& session);
static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options);
static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
- static void Logs(models::Session& session, const std::string& id, bool follow);
+ static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps = false, ULONGLONG since = 0, ULONGLONG until = 0, ULONGLONG tail = 0);
};
} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp
index 804f54282..a3102e2c3 100644
--- a/src/windows/wslc/tasks/ContainerTasks.cpp
+++ b/src/windows/wslc/tasks/ContainerTasks.cpp
@@ -392,6 +392,26 @@ void ViewContainerLogs(CLIExecutionContext& context)
auto& session = context.Data.Get();
auto containerId = context.Args.Get();
bool follow = context.Args.Contains(ArgType::Follow);
- ContainerService::Logs(session, WideToMultiByte(containerId), follow);
+ bool timestamps = context.Args.Contains(ArgType::Timestamps);
+
+ ULONGLONG tail = 0;
+ if (context.Args.Contains(ArgType::Tail))
+ {
+ tail = std::stoull(WideToMultiByte(context.Args.Get()));
+ }
+
+ ULONGLONG since = 0;
+ if (context.Args.Contains(ArgType::Since))
+ {
+ since = std::stoull(WideToMultiByte(context.Args.Get()));
+ }
+
+ ULONGLONG until = 0;
+ if (context.Args.Contains(ArgType::Until))
+ {
+ until = std::stoull(WideToMultiByte(context.Args.Get()));
+ }
+
+ ContainerService::Logs(session, WideToMultiByte(containerId), follow, timestamps, since, until, tail);
}
} // namespace wsl::windows::wslc::task
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerLogsTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerLogsTests.cpp
new file mode 100644
index 000000000..478da8469
--- /dev/null
+++ b/test/windows/wslc/e2e/WSLCE2EContainerLogsTests.cpp
@@ -0,0 +1,170 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ WSLCE2EContainerLogsTests.cpp
+
+Abstract:
+
+ This file contains end-to-end tests for WSLC.
+--*/
+
+#include "precomp.h"
+#include "windows/Common.h"
+#include "WSLCExecutor.h"
+#include "WSLCE2EHelpers.h"
+
+namespace WSLCE2ETests {
+using namespace wsl::shared;
+
+class WSLCE2EContainerLogsTests
+{
+ WSLC_TEST_CLASS(WSLCE2EContainerLogsTests)
+
+ TEST_CLASS_SETUP(ClassSetup)
+ {
+ EnsureImageIsLoaded(DebianImage);
+ return true;
+ }
+
+ TEST_CLASS_CLEANUP(ClassCleanup)
+ {
+ EnsureContainerDoesNotExist(WslcContainerName);
+ EnsureImageIsDeleted(DebianImage);
+ return true;
+ }
+
+ TEST_METHOD_SETUP(MethodSetup)
+ {
+ EnsureContainerDoesNotExist(WslcContainerName);
+ return true;
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Logs_HelpCommand)
+ {
+ auto result = RunWslc(L"container logs --help");
+ result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Logs_MissingContainerId)
+ {
+ auto result = RunWslc(L"container logs");
+ result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Logs_Success)
+ {
+ auto result = RunWslc(std::format(
+ L"container run --name {} {} sh -c \"echo hello && echo world\"", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container logs {}", WslcContainerName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ auto lines = result.GetStdoutLines();
+ VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), L"hello"));
+ VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), L"world"));
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Logs_TailOption)
+ {
+ auto result = RunWslc(std::format(
+ L"container run --name {} {} sh -c \"echo line1 && echo line2 && echo line3\"", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container logs --tail 1 {}", WslcContainerName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ auto lines = result.GetStdoutLines();
+ VERIFY_ARE_EQUAL(1u, lines.size());
+ VERIFY_ARE_EQUAL(L"line3", lines[0]);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Logs_TimestampsOption)
+ {
+ auto result =
+ RunWslc(std::format(L"container run --name {} {} sh -c \"echo hello\"", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ result = RunWslc(std::format(L"container logs --timestamps {}", WslcContainerName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ auto lines = result.GetStdoutLines();
+ VERIFY_IS_TRUE(lines.size() >= 1u);
+ // Timestamps are prepended in ISO 8601 / RFC 3339 format, verify by checking for 'T' separator
+ VERIFY_IS_TRUE(lines[0].find(L"T") != std::wstring::npos);
+ VERIFY_IS_TRUE(lines[0].find(L"hello") != std::wstring::npos);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Logs_FollowOption)
+ {
+ // Run a detached container that outputs lines with a delay between them
+ auto result = RunWslc(std::format(
+ L"container run -d --name {} {} sh -c \"echo first && sleep 2 && echo second\"", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ // Start following logs interactively — this blocks until the container exits
+ auto session = RunWslcInteractive(std::format(L"container logs --follow {}", WslcContainerName));
+ VERIFY_IS_TRUE(session.IsRunning(), L"Follow session should be running");
+
+ // Expect the first line of output
+ session.ExpectStdout("first\n");
+
+ // Expect the second line which comes after the sleep
+ session.ExpectStdout("second\n");
+
+ // The container exits after printing second, so follow should terminate
+ auto exitCode = session.Wait(30000);
+ VERIFY_ARE_EQUAL(0, exitCode);
+ }
+
+private:
+ const std::wstring WslcContainerName = L"wslc-e2e-container-logs";
+ const TestImage& DebianImage = DebianTestImage();
+
+ std::wstring GetHelpMessage() const
+ {
+ std::wstringstream output;
+ output << GetWslcHeader() //
+ << GetDescription() //
+ << GetUsage() //
+ << GetAvailableCommands() //
+ << GetAvailableOptions();
+ return output.str();
+ }
+
+ std::wstring GetDescription() const
+ {
+ return Localization::WSLCCLI_ContainerLogsLongDesc() + L"\r\n\r\n";
+ }
+
+ std::wstring GetUsage() const
+ {
+ return L"Usage: wslc container logs [] \r\n\r\n";
+ }
+
+ std::wstring GetAvailableCommands() const
+ {
+ std::wstringstream commands;
+ commands << L"The following arguments are available:\r\n" //
+ << L" container-id Container name or id\r\n" //
+ << L"\r\n";
+ return commands.str();
+ }
+
+ std::wstring GetAvailableOptions() const
+ {
+ std::wstringstream options;
+ options << L"The following options are available:\r\n" //
+ << L" --session Specify the session to use\r\n" //
+ << L" -f,--follow Follow log output\r\n" //
+ << L" -n,--tail Number of lines to show from the end of the logs\r\n" //
+ << L" --since Show logs since timestamp (e.g. unix timestamp)\r\n" //
+ << L" --until Show logs before timestamp (e.g. unix timestamp)\r\n" //
+ << L" --timestamps Show timestamps in log output\r\n" //
+ << L" -?,--help Shows help about the selected command\r\n" //
+ << L"\r\n";
+ return options.str();
+ }
+};
+} // namespace WSLCE2ETests