Skip to content

Commit 4355d10

Browse files
Merge pull request #131 from getsentry/feat/write-sentrylog
feat: Add Write-SentryLog cmdlet for structured logs
2 parents da0334b + 7818d05 commit 4355d10

6 files changed

Lines changed: 221 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Add `Write-SentryLog` cmdlet, a native PowerShell API for sending structured logs (Sentry Logs) ([#131](https://github.com/getsentry/sentry-powershell/pull/131))
8+
39
## 0.4.0
410

511
### Fixes

modules/Sentry/Sentry.psd1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
'Out-Sentry',
3939
'Start-Sentry',
4040
'Start-SentryTransaction',
41-
'Stop-Sentry'
41+
'Stop-Sentry',
42+
'Write-SentryLog'
4243
)
4344

4445
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
function Write-SentryLog {
2+
[CmdletBinding()]
3+
param(
4+
[Parameter(Mandatory, Position = 0, ValueFromPipeline = $true)]
5+
[AllowEmptyString()]
6+
[string] $Message,
7+
8+
[Sentry.SentryLogLevel] $Level = [Sentry.SentryLogLevel]::Info,
9+
10+
[object[]] $Parameters,
11+
12+
[hashtable] $Attributes
13+
)
14+
15+
process {
16+
if (-not [Sentry.SentrySdk]::IsEnabled) {
17+
try {
18+
Write-Debug 'Sentry is not started: Write-SentryLog invocation ignored.'
19+
} catch {}
20+
return
21+
}
22+
23+
$values = if ($null -eq $Parameters) { [object[]]@() } else { [object[]]$Parameters }
24+
$methodName = "Log$Level"
25+
26+
if ($null -ne $Attributes -and $Attributes.Count -gt 0) {
27+
$configureLog = [System.Action[Sentry.SentryLog]] {
28+
param([Sentry.SentryLog]$log)
29+
foreach ($key in $Attributes.Keys) {
30+
$log.SetAttribute([string]$key, $Attributes[$key])
31+
}
32+
}
33+
[Sentry.SentrySdk]::Logger.$methodName($configureLog, $Message, $values)
34+
} else {
35+
[Sentry.SentrySdk]::Logger.$methodName($Message, $values)
36+
}
37+
}
38+
}

samples/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ pwsh ./dependencies/download.ps1
99
Then you can run the sample, for example:
1010
```sh
1111
pwsh ./samples/locate-city.ps1 Toronto
12+
```
13+
14+
Or send structured logs to Sentry (see the [Sentry Logs docs](https://docs.sentry.io/platforms/dotnet/logs/)):
15+
```sh
16+
pwsh ./samples/send-logs.ps1
1217
```

samples/send-logs.ps1

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<#
2+
.SYNOPSIS
3+
Demonstrates sending structured logs to Sentry from PowerShell.
4+
.DESCRIPTION
5+
Shows how to enable Sentry Logs (https://docs.sentry.io/platforms/dotnet/logs/)
6+
in the PowerShell module and emit log messages at various severity levels,
7+
including templated messages with structured parameters and custom attributes.
8+
.EXAMPLE
9+
PS> ./send-logs.ps1
10+
.LINK
11+
https://docs.sentry.io/platforms/dotnet/logs/
12+
#>
13+
14+
# Import the Sentry module. In your code, you would just use `Import-Module Sentry`.
15+
Import-Module $PSScriptRoot/../modules/Sentry/Sentry.psd1
16+
17+
# Start the Sentry client. Set Experimental.EnableLogs = $true to opt in to Logs.
18+
Start-Sentry {
19+
$_.Dsn = 'https://997874440feaba4ecc65c1e25df7912b@o447951.ingest.us.sentry.io/4508073336176640'
20+
$_.Debug = $true
21+
$_.Experimental.EnableLogs = $true
22+
}
23+
24+
try {
25+
# Each call sends one log record at the given severity. Level defaults to Info.
26+
Write-SentryLog -Level Trace 'Trace from PowerShell'
27+
Write-SentryLog -Level Debug 'Debug from PowerShell'
28+
Write-SentryLog 'Info from PowerShell'
29+
Write-SentryLog -Level Warning 'Warning from PowerShell'
30+
Write-SentryLog -Level Error 'Error from PowerShell'
31+
Write-SentryLog -Level Fatal 'Fatal from PowerShell'
32+
33+
# Templated messages use positional placeholders ({0}, {1}, ...). Each value
34+
# passed via -Parameters is captured as a structured attribute on the log
35+
# record so you can search/filter on it in Sentry.
36+
$user = $env:USER ?? $env:USERNAME
37+
$hostName = [System.Net.Dns]::GetHostName()
38+
$psVersion = $PSVersionTable.PSVersion.ToString()
39+
Write-SentryLog -Level Info `
40+
-Message 'User {0} ran send-logs.ps1 on {1} (PowerShell {2})' `
41+
-Parameters $user, $hostName, $psVersion
42+
43+
# -Attributes attaches arbitrary key/value pairs to the log record.
44+
Write-SentryLog -Level Warning `
45+
-Message 'Disk usage on {0} is at {1}%' `
46+
-Parameters $hostName, 92 `
47+
-Attributes @{ region = 'us-east-1'; mount = '/var' }
48+
49+
# Logs are buffered and flushed in the background. Give them a moment to send.
50+
[Sentry.SentrySdk]::Flush([TimeSpan]::FromSeconds(5)) | Out-Null
51+
} finally {
52+
Stop-Sentry
53+
}

tests/write-sentrylog.tests.ps1

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
BeforeAll {
2+
. "$PSScriptRoot/utils.ps1"
3+
$global:SentryPowershellRethrowErrors = $true
4+
5+
function StartSentryForLogTests {
6+
Start-Sentry {
7+
$_.Dsn = 'https://key@127.0.0.1/1'
8+
$_.Experimental.EnableLogs = $true
9+
$_.Experimental.SetBeforeSendLog([System.Func[Sentry.SentryLog, Sentry.SentryLog]] {
10+
param([Sentry.SentryLog]$log)
11+
$script:logs.Add($log)
12+
return $null
13+
})
14+
$_.Transport = [RecordingTransport]::new()
15+
}
16+
}
17+
18+
# Logs go through a background batch processor; flush before asserting.
19+
function FlushLogs { [Sentry.SentrySdk]::Flush([TimeSpan]::FromSeconds(5)) | Out-Null }
20+
}
21+
22+
AfterAll {
23+
$global:SentryPowershellRethrowErrors = $false
24+
}
25+
26+
Describe 'Write-SentryLog' {
27+
BeforeEach {
28+
$script:logs = [System.Collections.Generic.List[Sentry.SentryLog]]::new()
29+
StartSentryForLogTests
30+
}
31+
32+
AfterEach {
33+
Stop-Sentry
34+
}
35+
36+
It 'sends an Info log by default' {
37+
Write-SentryLog 'hello'
38+
FlushLogs
39+
40+
$script:logs.Count | Should -Be 1
41+
$script:logs[0].Level | Should -Be ([Sentry.SentryLogLevel]::Info)
42+
$script:logs[0].Message | Should -Be 'hello'
43+
$script:logs[0].Template | Should -Be 'hello'
44+
}
45+
46+
It 'accepts the message from the pipeline' {
47+
'piped message' | Write-SentryLog -Level Warning
48+
FlushLogs
49+
50+
$script:logs.Count | Should -Be 1
51+
$script:logs[0].Level | Should -Be ([Sentry.SentryLogLevel]::Warning)
52+
$script:logs[0].Message | Should -Be 'piped message'
53+
}
54+
55+
It 'sends a log at each supported level' {
56+
Write-SentryLog -Level Trace 'trace'
57+
Write-SentryLog -Level Debug 'debug'
58+
Write-SentryLog -Level Info 'info'
59+
Write-SentryLog -Level Warning 'warning'
60+
Write-SentryLog -Level Error 'error'
61+
Write-SentryLog -Level Fatal 'fatal'
62+
FlushLogs
63+
64+
$script:logs.Count | Should -Be 6
65+
$script:logs[0].Level | Should -Be ([Sentry.SentryLogLevel]::Trace)
66+
$script:logs[1].Level | Should -Be ([Sentry.SentryLogLevel]::Debug)
67+
$script:logs[2].Level | Should -Be ([Sentry.SentryLogLevel]::Info)
68+
$script:logs[3].Level | Should -Be ([Sentry.SentryLogLevel]::Warning)
69+
$script:logs[4].Level | Should -Be ([Sentry.SentryLogLevel]::Error)
70+
$script:logs[5].Level | Should -Be ([Sentry.SentryLogLevel]::Fatal)
71+
}
72+
73+
It 'substitutes parameters into the template' {
74+
Write-SentryLog -Level Info -Message 'User {0} ran {1}' -Parameters 'alice', 'send-logs.ps1'
75+
FlushLogs
76+
77+
$script:logs.Count | Should -Be 1
78+
$script:logs[0].Template | Should -Be 'User {0} ran {1}'
79+
$script:logs[0].Message | Should -Be 'User alice ran send-logs.ps1'
80+
$script:logs[0].Parameters.Length | Should -Be 2
81+
}
82+
83+
It 'attaches structured attributes' {
84+
Write-SentryLog -Level Error -Message 'oops' -Attributes @{ user = 'bob'; retries = 3 }
85+
FlushLogs
86+
87+
$script:logs.Count | Should -Be 1
88+
$userValue = $null
89+
$script:logs[0].TryGetAttribute('user', [ref] $userValue) | Should -Be $true
90+
$userValue | Should -Be 'bob'
91+
$retriesValue = $null
92+
$script:logs[0].TryGetAttribute('retries', [ref] $retriesValue) | Should -Be $true
93+
$retriesValue | Should -Be 3
94+
}
95+
96+
It 'combines parameters and attributes in a single call' {
97+
Write-SentryLog -Level Warning `
98+
-Message 'host {0} is at {1}%' `
99+
-Parameters 'web-1', 92 `
100+
-Attributes @{ region = 'us-east-1' }
101+
FlushLogs
102+
103+
$script:logs.Count | Should -Be 1
104+
$script:logs[0].Message | Should -Be 'host web-1 is at 92%'
105+
$script:logs[0].Parameters.Length | Should -Be 2
106+
$regionValue = $null
107+
$script:logs[0].TryGetAttribute('region', [ref] $regionValue) | Should -Be $true
108+
$regionValue | Should -Be 'us-east-1'
109+
}
110+
}
111+
112+
Describe 'Write-SentryLog when Sentry is not started' {
113+
It 'is a no-op and does not throw' {
114+
[Sentry.SentrySdk]::IsEnabled | Should -Be $false
115+
{ Write-SentryLog -Level Info 'should be ignored' } | Should -Not -Throw
116+
}
117+
}

0 commit comments

Comments
 (0)