ScriptBeacon.psm1
Set-StrictMode -Version Latest #region Globals / Defaults $script:SBModuleVersion = '0.1.1' $script:DefaultBaseUri = $env:SCRIPTBEACON_BASE_URL if (-not $script:DefaultBaseUri) { $script:DefaultBaseUri = 'https://api.scriptbeacon.com' } $script:ConfigPath = $env:SCRIPTBEACON_CONFIG if (-not $script:ConfigPath) { $home = [Environment]::GetFolderPath('UserProfile') $script:ConfigPath = [IO.Path]::Combine($home, '.scriptbeacon', 'config.json') } #endregion #region Internal: Config + HTTP helpers function Get-SBConfig { [CmdletBinding()] param() $cfg = @{ BaseUri = $script:DefaultBaseUri BeaconId = $env:SCRIPTBEACON_BEACON_ID OrgId = $env:SCRIPTBEACON_ORG_ID ApiKey = $env:SCRIPTBEACON_API_KEY WriteSecret = $env:SCRIPTBEACON_WRITE_SECRET } if (Test-Path -LiteralPath $script:ConfigPath) { try { $file = Get-Content -Raw -LiteralPath $script:ConfigPath | ConvertFrom-Json -Depth 4 foreach ($k in 'BaseUri','BeaconId','OrgId','ApiKey','WriteSecret') { if ($file.PSObject.Properties[$k] -and $file.$k) { $cfg[$k] = $file.$k } } } catch {} } [pscustomobject]$cfg } function Save-SBConfigFile { param([psobject]$Config) $dir = [IO.Path]::GetDirectoryName($script:ConfigPath) if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $Config | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $script:ConfigPath -Encoding UTF8 } function Resolve-SBBaseUri { param([string]$BaseUri) if ($BaseUri) { return $BaseUri.TrimEnd('/') } (Get-SBConfig).BaseUri.TrimEnd('/') } function Require-SBBeaconId { param([string]$BeaconId) if ([string]::IsNullOrWhiteSpace($BeaconId)) { $BeaconId = (Get-SBConfig).BeaconId } if ([string]::IsNullOrWhiteSpace($BeaconId)) { throw "BeaconId is required. Use Set-SBConfig -BeaconId <uuid> first." } $BeaconId } function Test-SBFlatPrimitives { param([Parameter(Mandatory)][object]$Object) if ($null -eq $Object) { return $true } if ($Object -is [System.Collections.IDictionary]) { foreach ($k in $Object.Keys) { $v = $Object[$k] if ($null -eq $v) { continue } if ($v -is [string] -or $v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal] -or $v -is [bool]) { continue } return $false } return $true } elseif ($Object.PSObject -and $Object.PSObject.Properties.Count -gt 0) { foreach ($p in $Object.PSObject.Properties) { $v = $p.Value if ($null -eq $v) { continue } if ($v -is [string] -or $v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal] -or $v -is [bool]) { continue } return $false } return $true } return $false } function New-SBBodyJson { param([hashtable]$Data) $MAX_TEXT = 512 $flat = @{} foreach ($kv in $Data.GetEnumerator()) { $k = [string]$kv.Key; $v = $kv.Value if ($null -eq $v) { $flat[$k] = $null; continue } if ($v -is [string]) { $flat[$k] = $v.Substring(0,[Math]::Min($v.Length,$MAX_TEXT)) } elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal] -or $v -is [bool]) { $flat[$k] = $v } else { throw "Invalid value for key '$k'. Only primitive top-level values are allowed (string/number/bool/null)." } } $json = $flat | ConvertTo-Json -Depth 2 -Compress $bytes = [System.Text.Encoding]::UTF8.GetByteCount($json) if ($bytes -gt 16384) { throw "JSON payload is $bytes bytes; maximum allowed is 16384 bytes." } $json } function Invoke-SBRequest { [CmdletBinding()] param( [Parameter(Mandatory)][ValidateSet('GET','POST')][string]$Method, [Parameter(Mandatory)][string]$Path, [hashtable]$Query, [string]$JsonBody, [hashtable]$Headers, [string]$BaseUri ) $uriBase = Resolve-SBBaseUri -BaseUri $BaseUri $uri = "$uriBase$Path" if ($Query) { $pairs = foreach ($k in $Query.Keys) { $v = $Query[$k] if ($null -ne $v -and $v -ne '') { "{0}={1}" -f [uri]::EscapeDataString([string]$k), [uri]::EscapeDataString([string]$v) } } if ($pairs -and $pairs.Count) { $uri = ("{0}`?{1}" -f $uri, ($pairs -join '&')) } # escape ? in string } $hdr = @{ 'User-Agent' = "ScriptBeacon-PowerShell/$script:SBModuleVersion"; 'Accept'='application/json' } if ($Headers) { foreach ($k in $Headers.Keys) { $hdr[$k] = $Headers[$k] } } try { if ($Method -eq 'POST') { if ($JsonBody) { return Invoke-RestMethod -Method Post -Uri $uri -Headers $hdr -ContentType 'application/json' -Body $JsonBody -ErrorAction Stop } else { return Invoke-RestMethod -Method Post -Uri $uri -Headers $hdr -ErrorAction Stop } } else { return Invoke-RestMethod -Method Get -Uri $uri -Headers $hdr -ErrorAction Stop } } catch { $msg = $_.Exception.Message if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $msg = $_.ErrorDetails.Message } throw "Request failed: $msg" } } #endregion #region Public: Config function Set-SBConfig { [CmdletBinding(SupportsShouldProcess)] param( [string]$BeaconId,[string]$OrgId,[string]$ApiKey,[string]$BaseUri,[string]$WriteSecret, [switch]$Persist,[switch]$PassThru ) $cfg = Get-SBConfig if ($PSBoundParameters.ContainsKey('BeaconId')) { $cfg.BeaconId = $BeaconId } if ($PSBoundParameters.ContainsKey('OrgId')) { $cfg.OrgId = $OrgId } if ($PSBoundParameters.ContainsKey('ApiKey')) { $cfg.ApiKey = $ApiKey } if ($PSBoundParameters.ContainsKey('BaseUri')) { $cfg.BaseUri = $BaseUri } if ($PSBoundParameters.ContainsKey('WriteSecret')) { $cfg.WriteSecret= $WriteSecret } if ($Persist) { if ($PSCmdlet.ShouldProcess($script:ConfigPath,"Save ScriptBeacon config")) { Save-SBConfigFile -Config $cfg } } else { if ($cfg.BeaconId) { $env:SCRIPTBEACON_BEACON_ID = $cfg.BeaconId } if ($cfg.OrgId) { $env:SCRIPTBEACON_ORG_ID = $cfg.OrgId } if ($cfg.ApiKey) { $env:SCRIPTBEACON_API_KEY = $cfg.ApiKey } if ($cfg.BaseUri) { $env:SCRIPTBEACON_BASE_URL = $cfg.BaseUri } if ($cfg.WriteSecret) { $env:SCRIPTBEACON_WRITE_SECRET= $cfg.WriteSecret } } if ($PassThru) { return (Get-SBConfig) } } #endregion #region Public: Writers (quiet by default; -PassThru returns response) function Write-SBLog { [CmdletBinding()] param( [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][AllowNull()][string]$Message, [ValidateSet('info','warning','error','debug','trace','success')][string]$Level='info', [Alias('Tag')][string[]]$Tags, [Alias('Fields','Props')][hashtable]$Data, [string]$BeaconId,[string]$BaseUri,[string]$WriteSecret,[switch]$PassThru ) $bid = Require-SBBeaconId -BeaconId $BeaconId # accept comma-separated tags too if ($Tags -and $Tags.Count -eq 1 -and $Tags[0] -is [string] -and $Tags[0] -match ',') { $Tags = $Tags[0].Split(',') | ForEach-Object { $_.Trim() } } $body=@{}; if ($PSBoundParameters.ContainsKey('Message')) {$body.message=$Message}; $body.level=$Level if ($Tags) { $body.tags = ($Tags -join ',') } if ($Data) { if (-not (Test-SBFlatPrimitives -Object $Data)) { throw "Data must be a flat object with only primitive values (string/number/bool/null)." } foreach ($kv in $Data.GetEnumerator()) { $body[$kv.Key]=$kv.Value } } $json = New-SBBodyJson -Data $body $headers=@{}; $cfg=Get-SBConfig; $ws=$WriteSecret; if (-not $ws) { $ws=$cfg.WriteSecret }; if ($ws) { $headers['X-Beacon-Write']=$ws } $res = Invoke-SBRequest -Method POST -Path "/b/$bid" -JsonBody $json -Headers $headers -BaseUri $BaseUri if ($PassThru) { return $res } else { $null = $res; return } } function Send-SBHeartbeat { [CmdletBinding()] param( [switch]$Alive,[Nullable[datetime]]$Next,[TimeSpan]$Every,[Alias('Fields','Props')][hashtable]$Data, [string]$BeaconId,[string]$BaseUri,[string]$WriteSecret,[switch]$PassThru ) $bid = Require-SBBeaconId -BeaconId $BeaconId $q=@{}; if ($Every) { $q.next = (Get-Date).ToUniversalTime().Add($Every).ToString('o') } elseif ($Next) { $q.next = ([DateTime]$Next).ToUniversalTime().ToString('o') } $body=@{}; if ($Alive) { $body.heartbeat='alive' } if (-not $Alive -and $Data.Count -eq 0) { $body.heartbeat='alive' } if ($Data) { if (-not (Test-SBFlatPrimitives -Object $Data)) { throw "Data must be a flat object with only primitive values (string/number/bool/null)." } foreach ($kv in $Data.GetEnumerator()) { $body[$kv.Key]=$kv.Value } } $json = New-SBBodyJson -Data $body $headers=@{}; $cfg=Get-SBConfig; $ws=$WriteSecret; if (-not $ws) { $ws=$cfg.WriteSecret }; if ($ws) { $headers['X-Beacon-Write']=$ws } $res = Invoke-SBRequest -Method POST -Path "/b/$bid" -Query $q -JsonBody $json -Headers $headers -BaseUri $BaseUri if ($PassThru) { return $res } else { $null = $res; return } } function Send-SBInfo { [CmdletBinding()] param([Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][AllowNull()][string]$Message,[Alias('Tag')][string[]]$Tags,[Alias('Fields','Props')][hashtable]$Data,[string]$BeaconId,[string]$BaseUri,[string]$WriteSecret,[switch]$PassThru) process { if($PassThru){ return (Write-SBLog -Message $Message -Level info -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret -PassThru) } else { $null = Write-SBLog -Message $Message -Level info -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret } } } function Send-SBWarning { [CmdletBinding()] param([Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][AllowNull()][string]$Message,[Alias('Tag')][string[]]$Tags,[Alias('Fields','Props')][hashtable]$Data,[string]$BeaconId,[string]$BaseUri,[string]$WriteSecret,[switch]$PassThru) process { if($PassThru){ return (Write-SBLog -Message $Message -Level warning -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret -PassThru) } else { $null = Write-SBLog -Message $Message -Level warning -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret } } } function Send-SBError { [CmdletBinding()] param([Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][AllowNull()][string]$Message,[Alias('Tag')][string[]]$Tags,[Alias('Fields','Props')][hashtable]$Data,[string]$BeaconId,[string]$BaseUri,[string]$WriteSecret,[switch]$PassThru) process { if($PassThru){ return (Write-SBLog -Message $Message -Level error -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret -PassThru) } else { $null = Write-SBLog -Message $Message -Level error -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret } } } function Send-SBDebug { [CmdletBinding()] param([Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][AllowNull()][string]$Message,[Alias('Tag')][string[]]$Tags,[Alias('Fields','Props')][hashtable]$Data,[string]$BeaconId,[string]$BaseUri,[string]$WriteSecret,[switch]$PassThru) process { if($PassThru){ return (Write-SBLog -Message $Message -Level debug -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret -PassThru) } else { $null = Write-SBLog -Message $Message -Level debug -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret } } } Set-Alias -Name Beam-Info -Value Send-SBInfo Set-Alias -Name Beam-Warning -Value Send-SBWarning Set-Alias -Name Beam-Error -Value Send-SBError Set-Alias -Name Beam-Debug -Value Send-SBDebug Set-Alias -Name Beam-Heartbeat -Value Send-SBHeartbeat function Write-SBInfo { [CmdletBinding()] param([Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][AllowNull()][string]$Message,[Alias('Tag')][string[]]$Tags,[Alias('Fields','Props')][hashtable]$Data,[string]$BeaconId,[string]$BaseUri,[string]$WriteSecret,[switch]$PassThru) process { $r = Send-SBInfo -Message $Message -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret -PassThru:$PassThru; if($PassThru){return $r} } } function Write-SBWarning { [CmdletBinding()] param([Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][AllowNull()][string]$Message,[Alias('Tag')][string[]]$Tags,[Alias('Fields','Props')][hashtable]$Data,[string]$BeaconId,[string]$BaseUri,[string]$WriteSecret,[switch]$PassThru) process { $r = Send-SBWarning -Message $Message -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret -PassThru:$PassThru; if($PassThru){return $r} } } function Write-SBError { [CmdletBinding()] param([Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][AllowNull()][string]$Message,[Alias('Tag')][string[]]$Tags,[Alias('Fields','Props')][hashtable]$Data,[string]$BeaconId,[string]$BaseUri,[string]$WriteSecret,[switch]$PassThru) process { $r = Send-SBError -Message $Message -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret -PassThru:$PassThru; if($PassThru){return $r} } } function Write-SBDebug { [CmdletBinding()] param([Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][AllowNull()][string]$Message,[Alias('Tag')][string[]]$Tags,[Alias('Fields','Props')][hashtable]$Data,[string]$BeaconId,[string]$BaseUri,[string]$WriteSecret,[switch]$PassThru) process { $r = Send-SBDebug -Message $Message -Tags $Tags -Data $Data -BeaconId $BeaconId -BaseUri $BaseUri -WriteSecret $WriteSecret -PassThru:$PassThru; if($PassThru){return $r} } } #endregion #region Public: Reads function Get-SBEvents { [CmdletBinding()] param( [Parameter(Mandatory)][string]$OrgId,[Parameter(Mandatory)][string]$ApiKey, [string]$BeaconId,[int]$Limit=100,[string]$Cursor,[switch]$Before,[datetime]$Since,[datetime]$Until, [string]$EventType,[string]$Status,[string]$BaseUri ) $hdr=@{ Authorization="ApiKey $ApiKey" } $q=@{ limit=$Limit }; if ($Cursor){$q.cursor=$Cursor}; if ($Before){$q.before='1'} if ($BeaconId){$q.bleepId=$BeaconId}; if ($Since){$q.since=$Since.ToUniversalTime().ToString('o')} if ($Until){$q.until=$Until.ToUniversalTime().ToString('o')}; if ($EventType){$q.event_type=$EventType}; if ($Status){$q.status=$Status} Invoke-SBRequest -Method GET -Path "/org/$OrgId/events" -Query $q -Headers $hdr -BaseUri $BaseUri } function Get-SBEventsAll { [CmdletBinding()] param( [Parameter(Mandatory)][string]$OrgId,[Parameter(Mandatory)][string]$ApiKey, [string]$BeaconId,[int]$LimitTotal=1000,[string]$BaseUri ) $all=@(); $cursor=$null while ($all.Count -lt $LimitTotal) { $take=[Math]::Min(200,$LimitTotal-$all.Count) $page = Get-SBEvents -OrgId $OrgId -ApiKey $ApiKey -BeaconId $BeaconId -Limit $take -BaseUri $BaseUri -Cursor $cursor if ($page.items) { $all += $page.items } if (-not $page.next_cursor) { break } $cursor = $page.next_cursor } $all } function Get-SBBeacon { [CmdletBinding()] param([Parameter(Mandatory)][string]$BeaconId,[Parameter(Mandatory)][string]$ApiKey,[string]$BaseUri) $hdr=@{ Authorization="ApiKey $ApiKey" } Invoke-SBRequest -Method GET -Path "/beacon/$BeaconId" -Headers $hdr -BaseUri $BaseUri } function Get-SBBeaconStats { [CmdletBinding()] param([Parameter(Mandatory)][string]$BeaconId,[Parameter(Mandatory)][string]$ApiKey,[string]$Range='7d',[string]$Bucket='1d',[string]$BaseUri) $hdr=@{ Authorization="ApiKey $ApiKey" }; $q=@{ range=$Range; bucket=$Bucket } Invoke-SBRequest -Method GET -Path "/bleep/$BeaconId/stats" -Query $q -Headers $hdr -BaseUri $BaseUri } function Test-SBConnection { [CmdletBinding()] param([Parameter(Mandatory)][string]$OrgId,[Parameter(Mandatory)][string]$ApiKey,[string]$BaseUri) $sw=[Diagnostics.Stopwatch]::StartNew() try { $null = Get-SBEvents -OrgId $OrgId -ApiKey $ApiKey -Limit 1 -BaseUri $BaseUri; $sw.Stop(); [pscustomobject]@{ ok=$true; ms=$sw.ElapsedMilliseconds } } catch { $sw.Stop(); [pscustomobject]@{ ok=$false; ms=$sw.ElapsedMilliseconds; error=$_.Exception.Message } } } #endregion # Export public surface (aliases too) Export-ModuleMember -Function Get-SBConfig, Set-SBConfig, Write-SBLog, Write-SBInfo, Write-SBWarning, Write-SBError, Write-SBDebug, Send-SBInfo, Send-SBWarning, Send-SBError, Send-SBDebug, Send-SBHeartbeat, Get-SBEvents, Get-SBEventsAll, Get-SBBeacon, Get-SBBeaconStats, Test-SBConnection -Alias Beam-Info, Beam-Warning, Beam-Error, Beam-Debug, Beam-Heartbeat |