diff --git a/.github/skills/static-code-review/SKILL.md b/.github/skills/static-code-review/SKILL.md
new file mode 100644
index 00000000..b63d241c
--- /dev/null
+++ b/.github/skills/static-code-review/SKILL.md
@@ -0,0 +1,89 @@
+---
+name: static-code-review
+description: 'Perform evidence-based static code reviews for bugs, technical errors, security vulnerabilities, and performance or scaling risks without executing project code. Use when asked to review, audit, inspect, or assess a PowerShell module, Azure integration, build pipeline, or repository through read-only analysis.'
+argument-hint: 'Optionally specify files, folders, categories, or a strict no-execution constraint'
+user-invocable: true
+---
+
+# Static Code Review
+
+Review code through read-only analysis and report actionable findings before summaries. Prioritize defects that affect correctness, safety, security, or behavior at scale.
+
+## Scope And Constraints
+
+- Respect the files, symbols, categories, and depth requested by the user.
+- When no scope is supplied, cover all functions, module initialization, manifests, build and release paths, and relevant tests.
+- Do not edit files during a review unless the user separately asks for fixes.
+- In strict no-execution mode, do not run or import project code, scripts, modules, tests, builds, linters, analyzers, parsers, package managers, or cloud operations.
+- In strict no-execution mode, use only read-only file listing, file reading, text or semantic search, read-only exploration agents, repository history when needed, and authoritative documentation lookup.
+
+## Review Procedure
+
+1. Establish the review boundary and execution constraints from the request.
+2. Inventory the smallest set of public entry points that represents the requested surface.
+3. Trace each entry point to the code that directly constructs requests, mutates state, transforms data, resolves identity or configuration, or controls error and confirmation behavior.
+4. Inspect the module loader, manifest, build pipeline, and published-artifact assembly path. Distinguish source-tree behavior from generated release behavior.
+5. Inspect tests for behavioral coverage of the reviewed paths. Do not treat a missing test as a defect unless it leaves a concrete finding unprotected.
+6. Compare external request paths, API versions, body shapes, query parameters, enumerations, and response assumptions with authoritative vendor documentation.
+7. Search for security-sensitive behavior: destructive operations, confirmation bypasses, credential or token exposure, command or expression evaluation, mutable remote scripts, untrusted input crossing trust boundaries, and overly broad exports.
+8. Search for scaling behavior: repeated remote enumeration, per-item authentication or resolution, client-side filtering after broad list calls, missing pagination handling, unbounded materialization, and nested work whose cost grows with input size.
+9. Validate every candidate against nearby code, language semantics, build transformations, and external contracts. Remove findings that depend only on style, convention, or an unverified assumption.
+10. Report findings in descending severity with exact file and line references, impact, static evidence, and the condition that triggers the problem.
+
+## PowerShell Checks
+
+- Verify advanced functions that mutate state declare `SupportsShouldProcess` and pass `$PSCmdlet` through wrappers that implement `ShouldProcess`.
+- Trace whether `-WhatIf` and `-Confirm` reach the actual mutating operation rather than relying on help text or naming.
+- Check parameter mandatory status, parameter sets, validation attributes, aliases, default values, pipeline binding, and variable mutation across `begin`, `process`, and `end`.
+- Inspect list commands for wildcard routing, pagination, server-side versus client-side filtering, and subscription-wide enumeration.
+- Inspect manifests together with `Export-ModuleMember` and build-time manifest rewriting before claiming that internal functions are exported.
+- Inspect build scripts for incorrect source paths, omitted classes or enums, cleanup that removes uncompiled sources, and differences between local and published modules.
+- Do not claim that accessing a property such as `$null.Keys` throws unless strict mode or the actual runtime semantics make that true.
+
+## API Interaction Checks
+
+- Check request URI construction for missing resource segments, accidental collection endpoints, invalid interpolation, and path/query values derived from unchecked input.
+- Check request bodies against the selected API version, including property nesting, read-only fields, enum values, conditional properties, and service-specific types.
+- Verify service or cloud mappings are propagated to every request and helper call rather than replaced with hard-coded defaults.
+- Check converters for assumptions about resource ID shape, null input, collection responses, and property availability.
+
+## Candidate Validation
+
+Before reporting a finding, answer all of the following:
+
+- What exact input, configuration, or call path reaches it?
+- What observable failure, security impact, or scaling cost follows?
+- Which line directly causes the behavior?
+- Is the claim confirmed by local language semantics or an authoritative external contract?
+- Could a wrapper, build step, generated artifact, dependency behavior, or later guard invalidate the claim?
+- Is this a defect rather than a preference, hardening suggestion, or documentation issue?
+
+If any answer is missing, investigate one nearby hop or downgrade the item to an explicit risk or open question. Do not present speculation as a confirmed defect.
+
+## Severity
+
+- **Critical**: Prevents parsing, loading, building, or publishing the primary artifact; enables direct compromise; or causes catastrophic unintended mutation.
+- **High**: Breaks a core operation, bypasses an expected safety control, creates a practical security vulnerability, or sends materially invalid external requests.
+- **Medium**: Breaks an optional feature or common edge case, creates misleading behavior with operational impact, or scales poorly under realistic load.
+- **Low**: Limited correctness defect, hardening opportunity, or performance issue with a narrow trigger and small impact.
+
+Do not inflate severity merely because code calls a cloud API or performs deletion. Base severity on reachability and impact.
+
+## Reporting Format
+
+Lead with findings, ordered by severity. For each finding include:
+
+1. A concise impact-oriented title.
+2. A clickable file and line reference.
+3. The triggering path or input.
+4. The resulting behavior and why it matters.
+5. The decisive static evidence or external contract.
+
+After findings, include only relevant open questions, then a short review summary covering:
+
+- areas reviewed;
+- whether confirmed security or scaling defects were found;
+- test gaps tied to reported findings;
+- the no-execution limitation.
+
+If no actionable findings remain after validation, say so clearly and identify residual risks or unreviewed boundaries. Avoid padding the report with style observations.
\ No newline at end of file
diff --git a/PSFramework/PSFramework.psd1 b/PSFramework/PSFramework.psd1
index 79c920e2..a037dc98 100644
--- a/PSFramework/PSFramework.psd1
+++ b/PSFramework/PSFramework.psd1
@@ -4,7 +4,7 @@
RootModule = 'PSFramework.psm1'
# Version number of this module.
- ModuleVersion = '1.15.472'
+ ModuleVersion = '1.15.479'
# ID used to uniquely identify this module
GUID = '8028b914-132b-431f-baa9-94a6952f21ff'
diff --git a/PSFramework/bin/PSFramework.dll b/PSFramework/bin/PSFramework.dll
index 7c3e237a..04156d18 100644
Binary files a/PSFramework/bin/PSFramework.dll and b/PSFramework/bin/PSFramework.dll differ
diff --git a/PSFramework/bin/PSFramework.pdb b/PSFramework/bin/PSFramework.pdb
index a8eb9821..19fbf3c0 100644
Binary files a/PSFramework/bin/PSFramework.pdb and b/PSFramework/bin/PSFramework.pdb differ
diff --git a/PSFramework/bin/PSFramework.xml b/PSFramework/bin/PSFramework.xml
index 344457d1..8460a7eb 100644
--- a/PSFramework/bin/PSFramework.xml
+++ b/PSFramework/bin/PSFramework.xml
@@ -252,6 +252,16 @@
The key of the entry
The value matching the key
+
+
+ A threadsafe way to retrieve content from the cache.
+ This is the only way to do so and be able to distinguish between "Not cached" and "Cache entry exists, but is null".
+ As such this method is of limited use within PowerShell.
+
+ The key to retrieve the cached value for.
+ The value retrieved
+ Whether retrieval was successful.
+
A cache entry
@@ -1532,6 +1542,11 @@
The message to write
+
+
+ Writes a warning or error level message by the preferred color, if any.
+
+
Calculates the default color a message should have
@@ -3639,6 +3654,119 @@
The logging has been fully stopped
+
+
+ Contains the log entries retained by an in-memory log provider instance.
+
+
+
+
+ The name of the memory log
+
+
+
+
+ Maximum number of messages before the oldest entries get forgotten.
+
+
+
+
+ Maximum age of log entries before they get removed.
+
+
+
+
+ Should the memory log be archived when retired?
+ Otherwise, it will be lost on retirement.
+
+
+
+
+ Path to where logs should be written when retired.
+ LogEntries will be written as binary data blob and can be read in with Import-PSFClixml.
+
+
+
+
+ When the following runspace no longer exists, automatically retire this log.
+
+
+
+
+ Create a new in-memory log
+
+ Name of the log to create. Retires previous logs with this name.
+
+
+
+ When the MemoryLog was started.
+
+
+
+
+ When the MemoryLog was closed.
+
+
+
+
+ Whether the log has been archived
+
+
+
+
+ ID of the MemoryLog, to differentiate multiple archived logs with the same name
+
+
+
+
+ Messages that are logged
+
+
+
+
+ How many messages are in the log
+
+
+
+
+ The messages in the log
+
+
+
+
+ Add a new log entry to the log
+
+ The data to log
+
+
+
+ Remove all invalid log entries in the Log.
+ Not threadsafe.
+
+
+
+
+ Remove all invalid log entries in the Log.
+ Threadsafe.
+
+
+
+
+ Retrieve all log entries.
+
+ All logged entries.
+
+
+
+ Remove all log entries from the Log
+
+
+
+
+ Retire the current Log.
+ This unlists it from the log tracker and executes all "on archive" settings.
+
+
Class containing all information/content needed for a logging provider
@@ -4673,6 +4801,16 @@
Whether to use the error record for logging the callstack
The entry that is being written
+
+
+ The currently active InMemory logs of the inmemory Logging Provider
+
+
+
+
+ The list of archived InMemory logs of the inmemory Logging Provider
+
+
A rule object to change the default color of a message written to screen.
@@ -4939,6 +5077,16 @@
The color stuff gets written to the console in, when developer mode is enabled and the message would not have been written after all
+
+
+ The color with which to print warning messages on screen.
+
+
+
+
+ The color with which to print error messages on screen.
+
+
Enables the developer mode. In this all messages are written to the console, in order to make it easier to troubleshoot issues.
diff --git a/PSFramework/changelog.md b/PSFramework/changelog.md
index 296973aa..2b64db9a 100644
--- a/PSFramework/changelog.md
+++ b/PSFramework/changelog.md
@@ -1,5 +1,15 @@
# CHANGELOG
+## 1.15.479 (2026-10-27)
+
+- New: Logging Provider: inmemory - Writes messages to an in-memory list accessible through Get-PSFMessage.
+- New: Config PSFramework.Message.ErrorColor - The color to use when writing error messages on screen. This prevents the use of the warning stream!
+- New: Config PSFramework.Message.WarningColor - The color to use when writing warning messages on screen. This prevents the use of the warning stream!
+- Upd: PSFCache - added a `TryGetValue` method to 100% reliably retrieve value when a $null cache might be a legal state.
+- Upd: Get-PSFMessage - added `-LogName` parameter, allowing searching through the inmemory log of the new Logging Provider.
+- Upd: Get-PSFMessage - added `-AsLog` parameter, retrieving the entire log object, rather than its messages.
+- Upd: Clear-PSFMessage - add `-LogName` parameter, allowing the clearing of logs from the logs of the new Logging Provider
+
## 1.15.472 (2026-10-23)
- New: Logging Provider: Azure LogAnalytics V2 - sends logs to Log Analytics via Data Collection Rule
diff --git a/PSFramework/functions/logging/Set-PSFLoggingProvider.ps1 b/PSFramework/functions/logging/Set-PSFLoggingProvider.ps1
index 7adb3e77..8ac5208c 100644
--- a/PSFramework/functions/logging/Set-PSFLoggingProvider.ps1
+++ b/PSFramework/functions/logging/Set-PSFLoggingProvider.ps1
@@ -296,74 +296,74 @@
#region Filter Configuration
$setProperty = -not $InstanceName -or $InstanceName -eq "Default"
- if (Test-PSFParameterBinding -ParameterName "IncludeModules")
+ if (Test-PSFParameterBinding -ParameterName "IncludeModules" -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.IncludeModules = $IncludeModules }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeModules" -Value $IncludeModules
+ if ($setProperty) { $provider.IncludeModules = $PSBoundParameters.IncludeModules }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeModules" -Value $PSBoundParameters.IncludeModules
}
- if (Test-PSFParameterBinding -ParameterName "ExcludeModules")
+ if (Test-PSFParameterBinding -ParameterName "ExcludeModules" -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.ExcludeModules = $ExcludeModules }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeModules" -Value $ExcludeModules
+ if ($setProperty) { $provider.ExcludeModules = $PSBoundParameters.ExcludeModules }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeModules" -Value $PSBoundParameters.ExcludeModules
}
- if (Test-PSFParameterBinding -ParameterName "IncludeFunctions")
+ if (Test-PSFParameterBinding -ParameterName "IncludeFunctions" -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.IncludeFunctions = $IncludeFunctions }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeFunctions" -Value $IncludeFunctions
+ if ($setProperty) { $provider.IncludeFunctions = $PSBoundParameters.IncludeFunctions }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeFunctions" -Value $PSBoundParameters.IncludeFunctions
}
- if (Test-PSFParameterBinding -ParameterName "ExcludeFunctions")
+ if (Test-PSFParameterBinding -ParameterName "ExcludeFunctions" -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.ExcludeFunctions = $ExcludeFunctions }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeFunctions" -Value $ExcludeFunctions
+ if ($setProperty) { $provider.ExcludeFunctions = $PSBoundParameters.ExcludeFunctions }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeFunctions" -Value $PSBoundParameters.ExcludeFunctions
}
- if (Test-PSFParameterBinding -ParameterName "IncludeRunspaces")
+ if (Test-PSFParameterBinding -ParameterName "IncludeRunspaces" -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.IncludeRunspaces = $IncludeRunspaces }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeRunspaces" -Value $IncludeRunspaces
+ if ($setProperty) { $provider.IncludeRunspaces = $PSBoundParameters.IncludeRunspaces }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeRunspaces" -Value $PSBoundParameters.IncludeRunspaces
}
- if (Test-PSFParameterBinding -ParameterName "ExcludeRunspaces")
+ if (Test-PSFParameterBinding -ParameterName "ExcludeRunspaces" -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.ExcludeRunspaces = $ExcludeRunspaces }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeRunspaces" -Value $ExcludeRunspaces
+ if ($setProperty) { $provider.ExcludeRunspaces = $PSBoundParameters.ExcludeRunspaces }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeRunspaces" -Value $PSBoundParameters.ExcludeRunspaces
}
- if (Test-PSFParameterBinding -ParameterName "IncludeTags")
+ if (Test-PSFParameterBinding -ParameterName "IncludeTags" -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.IncludeTags = $IncludeTags }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeTags" -Value $IncludeTags
+ if ($setProperty) { $provider.IncludeTags = $PSBoundParameters.IncludeTags }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeTags" -Value $PSBoundParameters.IncludeTags
}
- if (Test-PSFParameterBinding -ParameterName "ExcludeTags")
+ if (Test-PSFParameterBinding -ParameterName "ExcludeTags" -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.ExcludeTags = $ExcludeTags }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeTags" -Value $ExcludeTags
+ if ($setProperty) { $provider.ExcludeTags = $PSBoundParameters.ExcludeTags }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)ExcludeTags" -Value $PSBoundParameters.ExcludeTags
}
- if ($MinLevel)
+ if (Test-PSFParameterBinding -ParameterName 'MinLevel' -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.MinLevel = $MinLevel }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)MinLevel" -Value $MinLevel
+ if ($setProperty) { $provider.MinLevel = $PSBoundParameters.MinLevel }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)MinLevel" -Value $PSBoundParameters.MinLevel
}
- if ($MaxLevel)
+ if (Test-PSFParameterBinding -ParameterName 'MaxLevel' -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.MaxLevel = $MaxLevel }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)MaxLevel" -Value $MaxLevel
+ if ($setProperty) { $provider.MaxLevel = $PSBoundParameters.MaxLevel }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)MaxLevel" -Value $PSBoundParameters.MaxLevel
}
- if (Test-PSFParameterBinding -ParameterName "ExcludeWarning")
+ if (Test-PSFParameterBinding -ParameterName "ExcludeWarning" -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.IncludeWarning = -not $ExcludeWarning }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeWarning" -Value (-not $ExcludeWarning)
+ if ($setProperty) { $provider.IncludeWarning = -not $PSBoundParameters.ExcludeWarning }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeWarning" -Value (-not $PSBoundParameters.ExcludeWarning)
}
- if (Test-PSFParameterBinding -ParameterName "ExcludeError")
+ if (Test-PSFParameterBinding -ParameterName "ExcludeError" -BoundParameters $PSBoundParameters)
{
- if ($setProperty) { $provider.IncludeError = -not $ExcludeError }
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeError" -Value (-not $ExcludeError)
+ if ($setProperty) { $provider.IncludeError = -not $PSBoundParameters.ExcludeError }
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)IncludeError" -Value (-not $PSBoundParameters.ExcludeError)
}
# V2 Only
- if (Test-PSFParameterBinding -ParameterName "RequiresInclude"){
- Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)RequiresInclude" -Value $RequiresInclude
+ if (Test-PSFParameterBinding -ParameterName "RequiresInclude" -BoundParameters $PSBoundParameters){
+ Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).$($instanceAffix)RequiresInclude" -Value $PSBoundParameters.RequiresInclude
}
#endregion Filter Configuration
diff --git a/PSFramework/functions/message/Clear-PSFMessage.ps1 b/PSFramework/functions/message/Clear-PSFMessage.ps1
index 3edec2c2..d4117b70 100644
--- a/PSFramework/functions/message/Clear-PSFMessage.ps1
+++ b/PSFramework/functions/message/Clear-PSFMessage.ps1
@@ -1,21 +1,45 @@
function Clear-PSFMessage {
<#
.SYNOPSIS
- Clears the in-memory log of the message system.
+ Clears the in-memory log of the message system, or the inmemory logs by the inmemory Logging Provider.
.DESCRIPTION
- Clears the in-memory log of the message system.
+ Clears the in-memory log of the message system, or the inmemory logs by the inmemory Logging Provider.
Has no effect on written logfiles, table entries, eventlog logs or wherever else you may be logging.
+
+ Defaults to the message system memory log unless specified.
+
+ .PARAMETER LogName
+ The name of the inmemory log to clear.
+ Maps to the InstanceName of the inmemory Logging Provider.
.EXAMPLE
PS C:\> Clear-PSFMessage
Clears the in-memory log of the message system.
+
+ .EXAMPLE
+ PS C:\> Clear-PSFMessage -LogName user-mgmt-log-max
+
+ Clears all in-memory logs of the 'user-mgmt-log-max' inmemory Logging Provider
#>
[CmdletBinding()]
- param ()
+ param (
+ [PsfArgumentCompleter('PSFramework.Logging.InMemoryLogName')]
+ [string]
+ $LogName
+ )
process {
- [PSFramework.Message.LogHost]::ClearLog()
+ if (-not $LogName) {
+ [PSFramework.Message.LogHost]::ClearLog()
+ return
+ }
+
+ $allLogs = [PSFramework.Message.LogHost]::InMemoryLogs.Values, [PSFramework.Message.LogHost]::InMemoryLogsArchive.Values | Write-Output | Where-Object {
+ $_.ID -eq $LogName -or
+ $_.Name -eq $LogName
+ }
+ foreach ($log in $allLogs) { $log.Clear() }
}
}
\ No newline at end of file
diff --git a/PSFramework/functions/message/Get-PSFMessage.ps1 b/PSFramework/functions/message/Get-PSFMessage.ps1
index e96f4ea3..b7ff0aa2 100644
--- a/PSFramework/functions/message/Get-PSFMessage.ps1
+++ b/PSFramework/functions/message/Get-PSFMessage.ps1
@@ -47,25 +47,35 @@
Will select the first 6 levels (Critical - SomewhatVerbose).
.PARAMETER Errors
- Instead of log entries, the error entries will be retrieved
+ Instead of log entries, the error entries will be retrieved.
+
+ .PARAMETER LogName
+ Instead of the message system log, access logs from an instance of the inmemory Logging Provider.
+
+ .PARAMETER AsLog
+ Return the entire log object of the instance of the inmemory Logging Provider.
.EXAMPLE
- Get-PSFMessage
+ PS C:\> Get-PSFMessage
Returns all log entries currently in memory.
+
+ .EXAMPLE
+ PS C:\> Get-PSFMessage -LogName group-cleanup
.EXAMPLE
- Get-PSFMessage -Target "a" -Last 1 -Skip 1
+ PS C:\> Get-PSFMessage -Target "a" -Last 1 -Skip 1
Returns all log entries that targeted the object "a" in the second last execution sent.
.EXAMPLE
- Get-PSFMessage -Tag "fail" -Last 5
+ PS C:\> Get-PSFMessage -Tag "fail" -Last 5
Returns all log entries within the last 5 executions that contained the tag "fail"
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
- [CmdletBinding(HelpUri = 'https://psframework.org/docs/Commands/PSFramework/Get-PSFMessage')]
+ [OutputType([PSFramework.Message.LogEntry])]
+ [CmdletBinding(DefaultParameterSetName = 'SelectDefault', HelpUri = 'https://psframework.org/docs/Commands/PSFramework/Get-PSFMessage')]
param (
[string]
$FunctionName = "*",
@@ -79,9 +89,11 @@
[string[]]
$Tag,
+ [Parameter(ParameterSetName = 'SelectDefault')]
[int]
$Last,
+ [Parameter(ParameterSetName = 'SelectDefault')]
[int]
$Skip = 0,
@@ -91,13 +103,31 @@
[PSFramework.Message.MessageLevel[]]
$Level,
+ [Parameter(Mandatory = $true, ParameterSetName = 'SelectErrors')]
+ [switch]
+ $Errors,
+
+ [Parameter(Mandatory = $true, ParameterSetName = 'SelectInMemoryLog')]
+ [PsfArgumentCompleter('PSFramework.Logging.InMemoryLogName')]
+ [string]
+ $LogName,
+
+ [Parameter(ParameterSetName = 'SelectInMemoryLog')]
[switch]
- $Errors
+ $AsLog
)
process
{
if ($Errors) { $messages = [PSFramework.Message.LogHost]::GetErrors() | Where-Object { ($_.FunctionName -like $FunctionName) -and ($_.ModuleName -like $ModuleName) } }
+ elseif ($LogName) {
+ $allLogs = [PSFramework.Message.LogHost]::InMemoryLogs.Values, [PSFramework.Message.LogHost]::InMemoryLogsArchive.Values | Write-Output | Where-Object {
+ $_.ID -eq $LogName -or
+ $_.Name -like $LogName
+ }
+ if ($AsLog) { return $allLogs }
+ $messages = $($allLogs.Messages)
+ }
else { $messages = [PSFramework.Message.LogHost]::GetLog() | Where-Object { ($_.FunctionName -like $FunctionName) -and ($_.ModuleName -like $ModuleName) } }
if (Test-PSFParameterBinding -ParameterName Target)
diff --git a/PSFramework/internal/configurations/message.ps1 b/PSFramework/internal/configurations/message.ps1
index 077fb771..09c08187 100644
--- a/PSFramework/internal/configurations/message.ps1
+++ b/PSFramework/internal/configurations/message.ps1
@@ -10,6 +10,8 @@ Set-PSFConfig -Module PSFramework -Name 'Message.Info.Color' -Value 'Cyan' -Init
Set-PSFConfig -Module PSFramework -Name 'Message.Info.Color.Emphasis' -Value 'green' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::InfoColorEmphasis = $_ } -Description "The color to use when emphasizing written text to the screen on PowerShell."
Set-PSFConfig -Module PSFramework -Name 'Message.Info.Color.Subtle' -Value 'gray' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::InfoColorSubtle = $_ } -Description "The color to use when making writing text to the screen on PowerShell appear subtle."
Set-PSFConfig -Module PSFramework -Name 'Message.DeveloperColor' -Value 'Gray' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::DeveloperColor = $_ } -Description "The color to use when writing text with developer specific additional information to the screen on PowerShell."
+Set-PSFConfig -Module PSFramework -Name 'Message.WarningColor' -Value $null -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::WarningColor = $_ } -Description "The color to use when writing warning messages on screen. This prevents the use of the warning stream!"
+Set-PSFConfig -Module PSFramework -Name 'Message.ErrorColor' -Value $null -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::ErrorColor = $_ } -Description "The color to use when writing error messages on screen. This prevents the use of the warning stream!"
Set-PSFConfig -Module PSFramework -Name 'Message.ConsoleOutput.Disable' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::DisableVerbosity = $_ } -Description "Global toggle that allows disabling all regular messages to screen. Messages from '-Verbose' and '-Debug' are unaffected"
Set-PSFConfig -Module PSFramework -Name 'Message.Transform.ErrorQueueSize' -Value 512 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.MessageHost]::TransformErrorQueueSize = $_ } -Description "The size of the queue for transformation errors. May be useful for advanced development, but can be ignored usually."
Set-PSFConfig -Module PSFramework -Name 'Message.NestedLevel.Decrement' -Value 0 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::NestedLevelDecrement = $_ } -Description "How many levels should be reduced per callstack depth. This makes commands less verbose, the more nested they are called"
diff --git a/PSFramework/internal/loggingProviders/inMemory.provider.ps1 b/PSFramework/internal/loggingProviders/inMemory.provider.ps1
new file mode 100644
index 00000000..e71627e6
--- /dev/null
+++ b/PSFramework/internal/loggingProviders/inMemory.provider.ps1
@@ -0,0 +1,87 @@
+$begin_event = {
+ $name = $script:Instance.Name
+ if (-not $name) { $name = 'Default' }
+ if ([PSFramework.Message.LogHost]::InMemoryLogs[$name]) { $script:logger = [PSFramework.Message.LogHost]::InMemoryLogs[$name] }
+ else { $script:logger = [PSFramework.Logging.MemoryLog]::new($name) }
+
+ $settings = Select-PSFConfig -FullName "$($script:Instance.InstanceCfgRoot).*" | Where-Object _FullName -EQ $script:Instance.InstanceCfgRoot
+
+ if ($null -ne $settings.MaxCount) { $script:logger.MaxCount = $settings.MaxCount }
+ if ($null -ne $settings.MaxAge) { $script:logger.MaxAge = $settings.MaxAge -as [PsfTimeSpan] }
+ if ($settings.ShouldArchive) { $script:logger.ShouldArchive = $true }
+ if ($settings.ArchivePath) {
+ $script:logger.ArchivePath = [PSFNewFile]$settings.ArchivePath
+ }
+
+ if ($settings.RunspaceBound -and $script:logger.RetireWithRunspace -eq [guid]::Empty) {
+ # At the end so it shows up in Get-PSFLoggingError, but still configures the rest of the logging.
+ throw "The RunspaceBound setting only works when defined through 'Set-PSFLoggingProvider'"
+ }
+}
+$message_event = {
+ param (
+ $Message
+ )
+
+ $script:logger.Add($Message)
+}
+
+$end_event = {
+ if ($script:logger.RetireWithRunspace -eq [guid]::Empty) { return }
+
+ $runspace = Get-Runspace -InstanceId $script:logger.RetireWithRunspace
+ if ($runspace) { return }
+
+ $disableName = "LoggingProvider.InMemory.Enabled"
+ if ($script:Instance.Name -and $script:Instance.Name -ne 'Default') { $disableName = "LoggingProvider.InMemory.$($script:Instance.Name).Enabled" }
+ Set-PSFConfig -FullName $disableName -Value $false
+}
+
+$final_event = {
+ $script:logger.Retire()
+}
+
+$configurationScript = {
+ param ($BoundParameters)
+
+ if ($BoundParameters.RunspaceBound) {
+ $effectiveInstanceName = $BoundParameters.InstanceName
+ if (-not $effectiveInstanceName) { $effectiveInstanceName = 'Default'}
+
+ # Prepare Runspace Expiration Content
+ if ([PSFramework.Message.LogHost]::InMemoryLogs[$effectiveInstanceName]) {
+ $logger = [PSFramework.Message.LogHost]::InMemoryLogs[$effectiveInstanceName]
+ }
+ else { $logger = [PSFramework.Logging.MemoryLog]::new($effectiveInstanceName) }
+ $logger.RetireWithRunspace = [runspace]::DefaultRunspace.InstanceId
+ $null = $BoundParameters.Remove('RunspaceBound')
+
+ # Add the current runspace to the runspace filter
+ $runspaces = @([runspace]::DefaultRunspace.InstanceId) + $BoundParameters.IncludeRunspaces | Remove-PSFNull
+ $BoundParameters['IncludeRunspaces'] = @($runspaces)
+ }
+}
+
+$configuration_Settings = {
+ Set-PSFConfig -Module 'PSFramework' -Name 'Logging.InMemory.MaxCount' -Value $null -Initialize -Validation long -Description 'Maximum number of messages in this log before retiring the oldest entries when adding.'
+ Set-PSFConfig -Module 'PSFramework' -Name 'Logging.InMemory.MaxAge' -Value $null -Initialize -Validation timespan -Description 'Maximum age entries may remain in the log. Entries beyond that limit will be removed as new entries are added.'
+ Set-PSFConfig -Module 'PSFramework' -Name 'Logging.InMemory.ShouldArchive' -Value $false -Initialize -Validation bool -Description 'Whether the inmemory log should remain available after closing the log within memory. If $false, Get-PSFMessage can only access the data until the logging provider is disabled.'
+ Set-PSFConfig -Module 'PSFramework' -Name 'Logging.InMemory.ArchivePath' -Value $null -Initialize -Validation string -Description 'When disabling the logging provider, write the data to this fully-valid file path. The folder must exist, relative paths may not be resolved as expected.'
+ Set-PSFConfig -Module 'PSFramework' -Name 'Logging.InMemory.RunspaceBound' -Value $false -Initialize -Validation bool -Description 'If set to true, this logging provider will filter for messages of the runspace calling Set-PSFLoggingProvider and automatically disable & archive when the runspace no longer exists. Note: Will error out if not defined via Set-PSFLoggingProvider. Runspaces may be recycled when used via RunspacePools.'
+}
+$paramRegisterPSFLoggingProvider = @{
+ Name = 'inmemory'
+ Description = 'Writes messages to an in-memory list accessible through Get-PSFMessage.'
+ Version2 = $true
+ ConfigurationRoot = 'PSFramework.Logging.InMemory'
+ InstanceProperties = 'MaxCount', 'MaxAge', 'ShouldArchive', 'ArchivePath', 'RunspaceBound'
+ MessageEvent = $message_Event
+ ConfigurationSettings = $configuration_Settings
+ ConfigurationDefaultValues = @{
+ RunspaceBound = $false
+ ShouldArchive = $false
+ }
+}
+
+# Register the Console logging provider
+Register-PSFLoggingProvider @paramRegisterPSFLoggingProvider -BeginEvent $begin_event -EndEvent $end_event -FinalEvent $final_event -ConfigurationScript $configurationScript
\ No newline at end of file
diff --git a/PSFramework/internal/scripts/loggingProviderInstanceModuleCode.ps1 b/PSFramework/internal/scripts/loggingProviderInstanceModuleCode.ps1
index 7ede0742..2cb851b3 100644
--- a/PSFramework/internal/scripts/loggingProviderInstanceModuleCode.ps1
+++ b/PSFramework/internal/scripts/loggingProviderInstanceModuleCode.ps1
@@ -3,30 +3,32 @@
$LoggingProviderInstance
)
- try
- {
+ try {
$module = New-Module -Name ([guid]::NewGuid()) -ArgumentList $LoggingProviderInstance -ScriptBlock {
param (
$LoggingProviderInstance
)
$Instance = [pscustomobject]@{
- Name = $LoggingProviderInstance.Name
- Provider = $LoggingProviderInstance.Provider.Name
+ Name = $LoggingProviderInstance.Name
+ Provider = $LoggingProviderInstance.Provider.Name
ConfigurationRoot = $LoggingProviderInstance.Provider.ConfigurationRoot
+ InstanceCfgRoot = $LoggingProviderInstance.Provider.ConfigurationRoot
+ }
+ if ($Instance.Name -and $Instance.Name -ne 'Default') {
+ $Instance.InstanceCfgRoot += ".$($script:Instance.Name)"
}
# Validate Language Mode for security reasons
- if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.BeginEvent -Mode 'ConstrainedLanguage') { throw "The event BeginEvent is in constrained language mode and cannot be loaded!" }
- if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.StartEvent -Mode 'ConstrainedLanguage') { throw "The event StartEvent is in constrained language mode and cannot be loaded!" }
- if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.MessageEvent -Mode 'ConstrainedLanguage') { throw "The event MessageEvent is in constrained language mode and cannot be loaded!" }
- if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.ErrorEvent -Mode 'ConstrainedLanguage') { throw "The event ErrorEvent is in constrained language mode and cannot be loaded!" }
- if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.EndEvent -Mode 'ConstrainedLanguage') { throw "The event EndEvent is in constrained language mode and cannot be loaded!" }
- if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.FinalEvent -Mode 'ConstrainedLanguage') { throw "The event FinalEvent is in constrained language mode and cannot be loaded!" }
+ if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.BeginEvent -Mode 'ConstrainedLanguage') { throw 'The event BeginEvent is in constrained language mode and cannot be loaded!' }
+ if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.StartEvent -Mode 'ConstrainedLanguage') { throw 'The event StartEvent is in constrained language mode and cannot be loaded!' }
+ if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.MessageEvent -Mode 'ConstrainedLanguage') { throw 'The event MessageEvent is in constrained language mode and cannot be loaded!' }
+ if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.ErrorEvent -Mode 'ConstrainedLanguage') { throw 'The event ErrorEvent is in constrained language mode and cannot be loaded!' }
+ if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.EndEvent -Mode 'ConstrainedLanguage') { throw 'The event EndEvent is in constrained language mode and cannot be loaded!' }
+ if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.FinalEvent -Mode 'ConstrainedLanguage') { throw 'The event FinalEvent is in constrained language mode and cannot be loaded!' }
- if ($LoggingProviderInstance.Provider.Functions)
- {
- if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.Functions -Mode "ConstrainedLanguage") { throw "The functions resource scriptblock is in constrained language mode and cannot be loaded!" }
+ if ($LoggingProviderInstance.Provider.Functions) {
+ if (Test-PSFLanguageMode -ScriptBlock $LoggingProviderInstance.Provider.Functions -Mode 'ConstrainedLanguage') { throw 'The functions resource scriptblock is in constrained language mode and cannot be loaded!' }
# Invoke in current scope after localizing the scriptblock into the current context
$LoggingProviderInstance.Provider.Functions.InvokeEx($false, $true, $false)
}
@@ -36,12 +38,11 @@
Start = [guid]::NewGuid()
Message = [guid]::NewGuid()
Error = [guid]::NewGuid()
- End = [guid]::NewGuid()
+ End = [guid]::NewGuid()
Final = [guid]::NewGuid()
}
- function Get-ConfigValue
- {
+ function Get-ConfigValue {
[CmdletBinding()]
param (
[string]
@@ -51,8 +52,7 @@
)
$rootPath = $script:Instance.ConfigurationRoot
- if ($script:Instance.Name -and $script:Instance.Name -ne "Default")
- {
+ if ($script:Instance.Name -and $script:Instance.Name -ne 'Default') {
$rootPath += ".$($script:Instance.Name)"
}
@@ -80,13 +80,11 @@
Export-ModuleMember
} -ErrorAction Stop
}
- catch
- {
+ catch {
$LoggingProviderInstance.Errors.Enqueue($_)
$LoggingProviderInstance.Enabled = $false
}
- if ($module)
- {
+ if ($module) {
$LoggingProviderInstance.Module = $module
}
}
\ No newline at end of file
diff --git a/PSFramework/internal/tepp/scripts/inmemory-logs.ps1 b/PSFramework/internal/tepp/scripts/inmemory-logs.ps1
new file mode 100644
index 00000000..bc82d61b
--- /dev/null
+++ b/PSFramework/internal/tepp/scripts/inmemory-logs.ps1
@@ -0,0 +1,14 @@
+Register-PSFTeppScriptblock -Name 'PSFramework.Logging.InMemoryLogName' -ScriptBlock {
+ $groups = [PSFramework.Message.LogHost]::InMemoryLogs.Values, [PSFramework.Message.LogHost]::InMemoryLogsArchive.Values | Write-Output | Group-Object Name | Sort-Object Name
+ foreach ($group in $groups) {
+ if ($group.Count -eq 1) {
+ @{ Text = $group.Name; Tooltip = 'ID: {0} | Started: {1:HH:mm:ss} | Count: {2} | Archived: {3}' -f $group.Group.ID, $group.Group.Started, $group.Group[0].Count, $group.Group.Archived }
+ continue
+ }
+
+ @{ Text = $group.Name; Tooltip = $group.Group.ForEach{ 'ID: {0} | Started: {1:HH:mm:ss} | Count: {2} | Archived: {3}' -f $_.ID, $_.Started, $_.Count, $_.Archived } -join "`n" }
+ foreach ($entry in $group.Group) {
+ @{ Text = $entry.ID; Tooltip = 'Name: {0} | Started: {1:HH:mm:ss} | Count: {2} | Archived: {3}' -f $entry.Name, $entry.Started, $entry.Count, $entry.Archived }
+ }
+ }
+} -Global
\ No newline at end of file
diff --git a/PSFramework/tests/general/FileIntegrity.Exceptions.ps1 b/PSFramework/tests/general/FileIntegrity.Exceptions.ps1
index 0ed2dda5..02a683e9 100644
--- a/PSFramework/tests/general/FileIntegrity.Exceptions.ps1
+++ b/PSFramework/tests/general/FileIntegrity.Exceptions.ps1
@@ -21,7 +21,7 @@ $global:MayContainCommand = @{
"Write-Verbose" = @()
"Write-Warning" = @('assembly.ps1')
"Write-Error" = @('Import-PSFJson.ps1', 'Import-PSFPowerShellDataFile.ps1', 'Invoke-PSFCommand.ps1', 'Set-PSFFileContent.ps1', 'Stop-PSFFunction.ps1', 'azureloganalytics2.provider.ps1')
- "Write-Output" = @('filesystem.provider.ps1', 'gelf.provider.ps1', 'Import-PSFPowerShellDataFile.ps1', 'logfile.provider.ps1', 'input.ps1', 'teppCoreCode.ps1')
+ "Write-Output" = @('Clear-PSFMessage.ps1','Get-PSFMessage.ps1','filesystem.provider.ps1', 'gelf.provider.ps1', 'Import-PSFPowerShellDataFile.ps1', 'logfile.provider.ps1', 'input.ps1', 'teppCoreCode.ps1','inmemory-logs.ps1')
"Write-Information" = @()
"Write-Debug" = @()
}
\ No newline at end of file
diff --git a/PSFramework/xml/PSFramework.Format.ps1xml b/PSFramework/xml/PSFramework.Format.ps1xml
index 458f97f8..2c5ba04e 100644
--- a/PSFramework/xml/PSFramework.Format.ps1xml
+++ b/PSFramework/xml/PSFramework.Format.ps1xml
@@ -342,6 +342,61 @@
+
+
+ PSFramework.Logging.MemoryLog
+
+ PSFramework.Logging.MemoryLog
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+ Count
+
+
+ Started
+
+
+ Stopped
+
+
+ Archived
+
+
+ ShouldArchive
+
+
+ MaxCount
+
+
+ MaxAge
+
+
+ ArchivePath
+
+
+
+
+
+
+
PSFramework.Logging.Provider
diff --git a/library/PSFramework/Caching/CacheMemoryConcurrent.cs b/library/PSFramework/Caching/CacheMemoryConcurrent.cs
index 5a211a4b..81de7f2d 100644
--- a/library/PSFramework/Caching/CacheMemoryConcurrent.cs
+++ b/library/PSFramework/Caching/CacheMemoryConcurrent.cs
@@ -156,5 +156,34 @@ public override object this[object key]
}
}
#endregion Core Overrides
+
+ ///
+ /// A threadsafe way to retrieve content from the cache.
+ /// This is the only way to do so and be able to distinguish between "Not cached" and "Cache entry exists, but is null".
+ /// As such this method is of limited use within PowerShell.
+ ///
+ /// The key to retrieve the cached value for.
+ /// The value retrieved
+ /// Whether retrieval was successful.
+ public bool TryGetValue(object Key, out object Value)
+ {
+ lock (_WriteLock)
+ {
+ if (!base.ContainsKey(Key))
+ {
+ Value = null;
+ return false;
+ }
+
+ // This can fail if collector code runs. TrySomething methods should not throw.
+ try { Value = this[Key]; }
+ catch
+ {
+ Value = null;
+ return false;
+ }
+ return true;
+ }
+ }
}
}
diff --git a/library/PSFramework/Commands/WritePSFMessageCommand.cs b/library/PSFramework/Commands/WritePSFMessageCommand.cs
index b82ed3a8..8fbc6e8d 100644
--- a/library/PSFramework/Commands/WritePSFMessageCommand.cs
+++ b/library/PSFramework/Commands/WritePSFMessageCommand.cs
@@ -482,7 +482,7 @@ whether this function was called from Stop-PSFFunction.
string onceName = String.Format("MessageOnce.{0}.{1}", FunctionName, Once);
if (!(Configuration.ConfigurationHost.Configurations.ContainsKey(onceName) && (bool)Configuration.ConfigurationHost.Configurations[onceName].Value))
{
- WriteWarning(_MessageStreams);
+ WriteWarningOrError();
channels = channels | LogEntryType.Warning;
Configuration.Config cfg = new Configuration.Config();
@@ -497,7 +497,7 @@ whether this function was called from Stop-PSFFunction.
}
else
{
- WriteWarning(_MessageStreams);
+ WriteWarningOrError();
channels = channels | LogEntryType.Warning;
}
}
@@ -965,6 +965,43 @@ private void WriteHostColor(string Message)
}
}
+ ///
+ /// Writes a warning or error level message by the preferred color, if any.
+ ///
+ private void WriteWarningOrError()
+ {
+ ConsoleColor color = ConsoleColor.Red;
+ if (Level == MessageLevel.Warning)
+ {
+ if (!MessageHost.WarningColor.HasValue)
+ {
+ WriteWarning(_MessageStreams);
+ return;
+ }
+ color = MessageHost.WarningColor.Value;
+ }
+ if (Level == MessageLevel.Error)
+ {
+ if (!MessageHost.ErrorColor.HasValue)
+ {
+ WriteWarning(_MessageStreams);
+ return;
+ }
+ color = MessageHost.ErrorColor.Value;
+ }
+
+
+ using (PowerShell ps = PowerShell.Create(RunspaceMode.CurrentRunspace))
+ {
+ ps.AddCommand("Write-PSFHostColor")
+ .AddParameter("String", _MessageHost)
+ .AddParameter("ErrorAction", "Ignore")
+ .AddParameter("DefaultColor", color);
+
+ ps.Invoke();
+ }
+ }
+
///
/// Calculates the default color a message should have
///
diff --git a/library/PSFramework/Logging/MemoryLog.cs b/library/PSFramework/Logging/MemoryLog.cs
new file mode 100644
index 00000000..57b4539e
--- /dev/null
+++ b/library/PSFramework/Logging/MemoryLog.cs
@@ -0,0 +1,200 @@
+using PSFramework.Message;
+using PSFramework.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PSFramework.Logging
+{
+ ///
+ /// Contains the log entries retained by an in-memory log provider instance.
+ ///
+ public class MemoryLog
+ {
+ #region Settings
+ ///
+ /// The name of the memory log
+ ///
+ public readonly string Name;
+
+ ///
+ /// Maximum number of messages before the oldest entries get forgotten.
+ ///
+ public long MaxCount;
+
+ ///
+ /// Maximum age of log entries before they get removed.
+ ///
+ public TimeSpan MaxAge;
+
+ ///
+ /// Should the memory log be archived when retired?
+ /// Otherwise, it will be lost on retirement.
+ ///
+ public bool ShouldArchive;
+
+ ///
+ /// Path to where logs should be written when retired.
+ /// LogEntries will be written as binary data blob and can be read in with Import-PSFClixml.
+ ///
+ public string ArchivePath;
+
+ ///
+ /// When the following runspace no longer exists, automatically retire this log.
+ ///
+ public Guid RetireWithRunspace;
+ #endregion Settings
+
+ ///
+ /// Create a new in-memory log
+ ///
+ /// Name of the log to create. Retires previous logs with this name.
+ public MemoryLog(string Name)
+ {
+ if (LogHost.InMemoryLogs.ContainsKey(Name))
+ {
+ // Archiving to path might fail
+ try { LogHost.InMemoryLogs[Name].Retire(); }
+ catch { }
+ }
+
+ this.Name = Name;
+ ID = Guid.NewGuid();
+ Started = DateTime.Now;
+ LogHost.InMemoryLogs[Name] = this;
+ }
+
+ #region Runtime Data
+ ///
+ /// When the MemoryLog was started.
+ ///
+ public readonly DateTime Started;
+
+ ///
+ /// When the MemoryLog was closed.
+ ///
+ public DateTime Stopped { get; private set; }
+
+ ///
+ /// Whether the log has been archived
+ ///
+ public bool Archived;
+
+ ///
+ /// ID of the MemoryLog, to differentiate multiple archived logs with the same name
+ ///
+ public readonly Guid ID;
+
+ ///
+ /// Messages that are logged
+ ///
+ private Queue _Messages = new Queue();
+
+ ///
+ /// How many messages are in the log
+ ///
+ public long Count => _Messages.Count;
+
+ ///
+ /// The messages in the log
+ ///
+ public LogEntry[] Messages => Get();
+ #endregion Runtime Data
+
+ #region Logic / Methods
+ ///
+ /// Add a new log entry to the log
+ ///
+ /// The data to log
+ public void Add(LogEntry Entry)
+ {
+ lock (this)
+ {
+ _Messages.Enqueue(Entry);
+ PurgeNoLock();
+ }
+ }
+
+ ///
+ /// Remove all invalid log entries in the Log.
+ /// Not threadsafe.
+ ///
+ private void PurgeNoLock()
+ {
+ if (MaxCount > 0)
+ while (_Messages.Count > MaxCount)
+ _Messages.Dequeue();
+
+ if (MaxAge != null && MaxAge.TotalMilliseconds > 0)
+ {
+ DateTime limit = DateTime.Now.Add(MaxAge.Negate());
+ while (_Messages.Peek().Timestamp < limit)
+ _Messages.Dequeue();
+ }
+
+ }
+
+ ///
+ /// Remove all invalid log entries in the Log.
+ /// Threadsafe.
+ ///
+ public void Purge()
+ {
+ lock (this)
+ {
+ PurgeNoLock();
+ }
+ }
+
+ ///
+ /// Retrieve all log entries.
+ ///
+ /// All logged entries.
+ public LogEntry[] Get()
+ {
+ lock (this)
+ {
+ return _Messages.ToArray();
+ }
+ }
+
+ ///
+ /// Remove all log entries from the Log
+ ///
+ public void Clear()
+ {
+ lock (this)
+ {
+ _Messages.Clear();
+ }
+ }
+
+ ///
+ /// Retire the current Log.
+ /// This unlists it from the log tracker and executes all "on archive" settings.
+ ///
+ public void Retire()
+ {
+ lock (this)
+ {
+ Stopped = DateTime.Now;
+ if (ShouldArchive)
+ LogHost.InMemoryLogsArchive[ID.ToString()] = this;
+
+ Archived = true;
+ MemoryLog temp;
+ LogHost.InMemoryLogs.TryRemove(Name, out temp);
+
+ if (String.IsNullOrEmpty(ArchivePath))
+ return;
+
+ // Intentionally placed after unlisting it from storage
+ // Bloated memory is considered worse than lost data
+ System.IO.File.WriteAllBytes(ArchivePath, ClixmlSerializer.ToByteCompressed(Get()));
+ }
+ }
+ #endregion Logic / Methods
+ }
+}
diff --git a/library/PSFramework/Message/LogHost.cs b/library/PSFramework/Message/LogHost.cs
index 654d0dec..9a9754a7 100644
--- a/library/PSFramework/Message/LogHost.cs
+++ b/library/PSFramework/Message/LogHost.cs
@@ -1,4 +1,5 @@
-using System;
+using PSFramework.Logging;
+using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -275,5 +276,16 @@ public static LogEntry WriteLogEntry(string Message, LogEntryType Type, DateTime
return temp;
}
#endregion Access Queues
+
+ #region In-Memory Logging
+ ///
+ /// The currently active InMemory logs of the inmemory Logging Provider
+ ///
+ public static ConcurrentDictionary InMemoryLogs = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+ ///
+ /// The list of archived InMemory logs of the inmemory Logging Provider
+ ///
+ public static ConcurrentDictionary InMemoryLogsArchive = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+ #endregion In-Memory Logging
}
}
diff --git a/library/PSFramework/Message/MessageHost.cs b/library/PSFramework/Message/MessageHost.cs
index d93dfd3a..7abea9ae 100644
--- a/library/PSFramework/Message/MessageHost.cs
+++ b/library/PSFramework/Message/MessageHost.cs
@@ -61,6 +61,16 @@ public static class MessageHost
///
public static ConsoleColor DeveloperColor = ConsoleColor.Gray;
+ ///
+ /// The color with which to print warning messages on screen.
+ ///
+ public static Nullable WarningColor;
+
+ ///
+ /// The color with which to print error messages on screen.
+ ///
+ public static Nullable ErrorColor;
+
///
/// Enables the developer mode. In this all messages are written to the console, in order to make it easier to troubleshoot issues.
///
diff --git a/library/PSFramework/PSFramework.csproj b/library/PSFramework/PSFramework.csproj
index f2a51c11..5c2aaaeb 100644
--- a/library/PSFramework/PSFramework.csproj
+++ b/library/PSFramework/PSFramework.csproj
@@ -135,6 +135,7 @@
+