Skip to content

Commit 303c7be

Browse files
committed
Implement logger module
1 parent cb3edde commit 303c7be

4 files changed

Lines changed: 317 additions & 0 deletions

File tree

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,29 @@ Get-Command -Module containers-toolkit
9797
Install-Containerd -Version "1.7.7" -InstallPath 'C:\Test\Path\Containerd'
9898
```
9999
100+
### Logging
101+
102+
The module uses a static logger designed for use across module files within the **Containers Toolkit**. It supports configurable log levels, console output, optional log file writing, and integration with the **Windows Event Log**.
103+
The logger supports the following log levels:
104+
105+
- `DEBUG`
106+
- `INFO`
107+
- `WARNING`
108+
- `ERROR`
109+
- `FATAL`
110+
111+
For more details on the logger, please refer to the [Logger documentation](./docs/LOGGER.md).
112+
113+
#### Logging Environment Variables
114+
115+
The logger uses the following environment variables to configure its behavior:
116+
117+
| Variable | Description |
118+
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
119+
| `$env:CTK_LOG_LEVEL` | Sets the minimum log level. Accepted values are: `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `FATAL`. Defaults to `INFO` if not set. |
120+
| `$env:CTK_LOG_FILE` | Path to a file where logs should be written. If not set, logs will be written to the console and Windows Event Log (for applicable levels). **Note:** The logger does not handle log file rotation or cleanup—use external tooling for that. |
121+
| `$env:SKIP_CTK_LOGGING` | If set to `"true"`, suppresses console output for all log levels except `DEBUG` (when `$DebugPreference` is not `"SilentlyContinue"`). Logging to file (if set) and to the Windows Event Log (excluding `DEBUG`) still occurs. |
122+
100123
## Important Notes
101124
102125
1. Requires elevated PowerShell to run some commands.
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
class Logger {
2+
static [string] $EventSource = "Containers-Toolkit"
3+
static [string] $EventLogName = "Application"
4+
static [string] $LogFile
5+
static [bool] $Quiet
6+
static [string] $MinLevel
7+
8+
static [hashtable] $LogLevelRank = @{
9+
"DEBUG" = 1
10+
"INFO" = 2
11+
"WARNING" = 3
12+
"ERROR" = 4
13+
"FATAL" = 5
14+
}
15+
16+
# Set minimum log level from environment variable: CTK_LOG_LEVEL or DebugPreference
17+
static [string] GetMinLevel() {
18+
try {
19+
$DebugPref = Get-Variable -Name DebugPreference -Scope Global -ValueOnly
20+
if ($DebugPref -ne "SilentlyContinue") {
21+
return "DEBUG"
22+
}
23+
elseif ($env:CTK_LOG_LEVEL) {
24+
return $env:CTK_LOG_LEVEL.ToUpper()
25+
}
26+
else {
27+
return "INFO"
28+
}
29+
}
30+
catch {
31+
return "INFO"
32+
}
33+
}
34+
35+
# Set log file path from environment variable: CTK_LOG_FILE
36+
static [string] GetLogFile() {
37+
try {
38+
$fileName = $env:CTK_LOG_FILE
39+
}
40+
catch {
41+
$fileName = $null
42+
}
43+
return $fileName
44+
}
45+
46+
# Set quiet mode from environment variable: SKIP_CTK_LOGGING
47+
# User-defined environment variable to skip logging. Equivalent to --quiet.
48+
# If set, only DEBUG messages are logged to the terminal.
49+
# If not set, all messages are logged to the terminal and to the event log.
50+
static [bool] GetQuiet() {
51+
try {
52+
$quietValue = $env:SKIP_CTK_LOGGING
53+
return if ($quietValue) { [bool]::Parse($quietValue) } else { $false }
54+
}
55+
catch {
56+
return $false
57+
}
58+
}
59+
60+
# Check if the log level is greater than or equal to the minimum log level
61+
static [bool] ShouldLog([string] $Level) {
62+
return [Logger]::LogLevelRank[$Level.ToUpper()] -ge [Logger]::LogLevelRank[[Logger]::MinLevel]
63+
}
64+
65+
# Format the message for logging
66+
static [string] FormatMessage([object] $message) {
67+
if ($null -eq $message) {
68+
return "[null]"
69+
}
70+
71+
if ($message -is [string]) {
72+
return $message
73+
}
74+
75+
try {
76+
return $message | ConvertTo-Json -Depth 5 -Compress
77+
}
78+
catch {
79+
return $message.ToString()
80+
# $Message = $Message | Out-String
81+
}
82+
}
83+
84+
# Retrieve the function in the call stack that triggered the log
85+
static [pscustomobject] GetCallerFunction($CallStack) {
86+
$i = 3
87+
$CallerFunction = $CallStack[$i]
88+
89+
while ((-not $CallerFunction.Command) -and ($i -lt $CallStack.Count - 1)) {
90+
$i++
91+
$CallerFunction = $CallStack[$i]
92+
}
93+
94+
return $CallerFunction
95+
}
96+
97+
# Generate a parsed log message from the log level and message
98+
static [string] GetLogMessage([string] $Level, [string] $Message) {
99+
$CallStack = Get-PSCallStack
100+
$Caller = [Logger]::GetCallerFunction($CallStack)
101+
102+
$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffK")
103+
$cmd = $Caller.InvocationInfo.MyCommand
104+
$line = $Caller.ScriptLineNumber
105+
106+
# Add padding to the message based on the difference between the longest level name and the current level
107+
$padding = ([Logger]::LogLevelRank.Keys | Measure-Object -Maximum Length).Maximum - $Level.Length
108+
109+
return "$($Level.ToUpper()):$(" " * $padding)[$timestamp] [${cmd}:${line}]: $Message"
110+
}
111+
112+
# Write log messages to the Windows Event Log
113+
static [void] WriteToEventLog([string] $Level, [string] $Message) {
114+
# Create the event log source if it doesn't exist
115+
if (-not [System.Diagnostics.EventLog]::SourceExists($([Logger]::EventLogName))) {
116+
Write-Debug "Creating event log source: { LogName: $([Logger]::EventLogName), Source: $([Logger]::EventSource) }"
117+
New-EventLog -LogName $([Logger]::EventLogName) -Source $([Logger]::EventSource)
118+
}
119+
120+
$entryType = switch ($Level) {
121+
"INFO" { [System.Diagnostics.EventLogEntryType]::Information }
122+
"WARNING" { [System.Diagnostics.EventLogEntryType]::Warning }
123+
"ERROR" { [System.Diagnostics.EventLogEntryType]::Error }
124+
"FATAL" { [System.Diagnostics.EventLogEntryType]::Error }
125+
default { throw [System.NotImplementedException]("Invalid log level: $Level") }
126+
}
127+
128+
$eventId = switch ($Level) {
129+
"INFO" { 1000 }
130+
"WARNING" { 4000 }
131+
"ERROR" { 5000 }
132+
"FATAL" { 6000 }
133+
default { throw [System.NotImplementedException]("Invalid log level: $Level") }
134+
}
135+
136+
try {
137+
Write-EventLog -LogName $([Logger]::EventLogName) `
138+
-Source $([Logger]::EventSource) `
139+
-EntryType $entryType `
140+
-EventId $eventId `
141+
-Message $Message
142+
}
143+
catch {
144+
# Fallback: write warning but continue
145+
Write-Warning "Failed to write to event log: $_"
146+
}
147+
}
148+
149+
# Write log messages to the console and/or event log (or file)
150+
# This is the main logging function that handles all log levels
151+
static [void] Write([string] $Level, [object] $Message) {
152+
# Set values
153+
[Logger]::MinLevel = [Logger]::GetMinLevel()
154+
[Logger]::LogFile = [Logger]::GetLogFile()
155+
[Logger]::Quiet = [Logger]::GetQuiet()
156+
157+
$Level = $Level.ToUpper()
158+
159+
# Minimum log level filtering: Only log messages that are at least as severe as the minimum level
160+
if (-not [Logger]::ShouldLog($Level)) {
161+
return
162+
}
163+
164+
# Convert the message to a string
165+
$formatedMessage = [Logger]::FormatMessage($message)
166+
167+
# Generate the log message
168+
$parsedMessage = [Logger]::GetLogMessage($Level, $formatedMessage)
169+
170+
# Default: Log to event log (non-DEBUG messages)
171+
if ($Level -ne "DEBUG") {
172+
[Logger]::WriteToEventLog($Level, $parsedMessage)
173+
}
174+
175+
# Log to file if CTK_LOG_FILE is set
176+
if ([Logger]::LogFile) {
177+
Add-Content -Path [Logger]::LogFile -Value $parsedMessage
178+
}
179+
180+
# If true, only DEBUG messages are logged to the terminal
181+
# else, all messages are logged to the terminal and to the event log.
182+
if ([Logger]::Quiet -and $Level -ne "DEBUG") {
183+
return
184+
}
185+
186+
# Console output
187+
switch ($Level) {
188+
"FATAL" { [Console]::Error.WriteLine($parsedMessage); throw $Message }
189+
"ERROR" { [Console]::Error.WriteLine($parsedMessage) }
190+
default { [Console]::WriteLine($parsedMessage) }
191+
}
192+
}
193+
194+
static [void] Fatal([object] $Message) { [Logger]::Write("FATAL", $Message) }
195+
static [void] Error([object] $Message) { [Logger]::Write("ERROR", $Message) }
196+
static [void] Warning([object] $Message) { [Logger]::Write("WARNING", $Message) }
197+
static [void] Info([object] $Message) { [Logger]::Write("INFO", $Message) }
198+
static [void] Debug([object] $Message) { [Logger]::Write("DEBUG", $Message) }
199+
static [void] Log([string] $Level = "INFO", [string] $Message) { [Logger]::Write($Level, $Message) }
200+
}

containers-toolkit/containers-toolkit.psd1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Note that the HostNetworkingService module is available only when the Hyper-V Wi
6868
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
6969
NestedModules = @(
7070
'Private\CommonToolUtilities.psm1',
71+
'Private\Logger.psm1',
7172
'Private\UpdateEnvironmentPath.psm1',
7273
'Public\AllToolsUtilities.psm1',
7374
'Public\BuildkitTools.psm1',
@@ -124,6 +125,7 @@ Note that the HostNetworkingService module is available only when the Hyper-V Wi
124125
# List of all files packaged with this module
125126
FileList = @(
126127
'./Private/CommonToolUtilities.psm1',
128+
'./Private/Logger.psm1',
127129
'./Private/UpdateEnvironmentPath.psm1',
128130
'./Public/AllToolsUtilities.psm1',
129131
'./Public/BuildkitTools.psm1',

docs/LOGGER.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Containers Toolkit Logger
2+
3+
A static PowerShell logger designed for use across module files within the **Containers Toolkit**. It supports configurable log levels, console output, optional log file writing, and integration with the **Windows Event Log**.
4+
5+
## Key Features
6+
7+
- Supports multiple log levels: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `FATAL`.
8+
- Logs messages to the **console**, **optional log file**, and the **Windows Event Log** (for applicable levels).
9+
- Minimum log level is determined dynamically from the `CTK_LOG_LEVEL` environment variable or PowerShell’s `$DebugPreference`.
10+
- Allows suppressing console output using the SKIP_CTK_LOGGING environment variable — similar to running in a quiet mode.
11+
12+
## Debug Logging Behavior
13+
14+
- `DEBUG` messages are only shown in the console if:
15+
16+
- `$DebugPreference` is not `"SilentlyContinue"`, **or**
17+
- the environment variable `CTK_LOG_LEVEL` is set to `"DEBUG"`.
18+
- `DEBUG` messages are **not** written to the Windows Event Log.
19+
20+
## Usage
21+
22+
To use the logger, you need to import the module (if it is not already imported).
23+
24+
```PowerShell
25+
using using module "..\Private\logger.psm1"
26+
```
27+
28+
## Log Levels
29+
30+
The logger supports the following log levels:
31+
32+
### Info level
33+
34+
```PowerShell
35+
[Logger]::Log("This is a test message") # Defaults to INFO level
36+
[Logger]::Log("This is a test message", "INFO")
37+
[Logger]::Info("This is a test message")
38+
39+
INFO: [2025-05-20T08:23:12Z] [Install-Nerdctl:42]: "This is a test message"
40+
```
41+
42+
### Debug level
43+
44+
To enable `DEBUG` level logging, set the environment variable `CTK_LOG_LEVEL` to `"DEBUG"` or ensure `$DebugPreference` is not set to `"SilentlyContinue"`.
45+
46+
```PowerShell
47+
[Logger]::Log("This is a test message", "DEBUG")
48+
[Logger]::Debug("This is a test message")
49+
50+
DEBUG: [2025-05-20T08:23:12Z] [Install-Nerdctl:42]: "This is a test message"
51+
```
52+
53+
### Warning level
54+
55+
```PowerShell
56+
[Logger]::Log("This is a test message", "WARNING")
57+
[Logger]::Warning("This is a test message")
58+
59+
WARNING: [2025-05-20T08:23:12Z] [Install-Nerdctl:42]: "This is a test message"
60+
```
61+
62+
### Error level
63+
64+
```PowerShell
65+
[Logger]::Log("This is a test message", "ERROR")
66+
[Logger]::Error("This is a test message")
67+
68+
ERROR: [2025-05-20T08:23:12Z] [Install-Nerdctl:42]: "This is a test message"
69+
```
70+
71+
### Fatal level
72+
73+
Throws a terminating error.
74+
75+
```PowerShell
76+
[Logger]::Log("This is a test message", "FATAL")
77+
[Logger]::Fatal("This is a test message")
78+
79+
80+
FATAL: [2025-05-20T08:23:12Z] [Install-Nerdctl:42]: "This is a test message"
81+
Exception: Uncaught Critical message
82+
```
83+
84+
## Environment Variables
85+
86+
The logger uses the following environment variables to configure its behavior:
87+
88+
| Variable | Description |
89+
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
90+
| `$env:CTK_LOG_LEVEL` | Sets the minimum log level. Accepted values are: `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `FATAL`. Defaults to `INFO` if not set. |
91+
| `$env:CTK_LOG_FILE` | Path to a file where logs should be written. If not set, logs will be written to the console and Windows Event Log (for applicable levels). **Note:** The logger does not handle log file rotation or cleanup—use external tooling for that. |
92+
| `$env:SKIP_CTK_LOGGING` | If set to `"true"`, suppresses console output for all log levels except `DEBUG` (when `$DebugPreference` is not `"SilentlyContinue"`). Logging to file (if set) and to the Windows Event Log (excluding `DEBUG`) still occurs. |

0 commit comments

Comments
 (0)