481 lines
18 KiB
PowerShell
481 lines
18 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
|
[string]$AnalyticsHost = '',
|
|
[string]$AnalyticsUser = 'igor',
|
|
[string]$RemoteRoot = '/opt/activitywatch/clickhouse-1c/landing',
|
|
[string]$RemoteKeyPath = 'C:\ProgramData\AWatch-rus\ssh\awops_ed25519'
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$LogDir = 'C:\ProgramData\AWatch-rus\logs'
|
|
$LogPath = Join-Path $LogDir 'file1c-telemetry.log'
|
|
$ScpExe = Join-Path $env:WINDIR 'System32\OpenSSH\scp.exe'
|
|
$ExporterStatePath = Join-Path (Split-Path -Parent $ConfigPath) 'file1c-telemetry-state.json'
|
|
|
|
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
|
|
|
|
function Write-RunLog {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Message
|
|
)
|
|
|
|
$line = '{0} {1}' -f ([DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')), $Message
|
|
Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8
|
|
}
|
|
|
|
trap {
|
|
Write-RunLog ("ERROR: " + ($_ | Out-String).Trim())
|
|
exit 1
|
|
}
|
|
|
|
function New-TemporarySshKeyCopy {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$SourceKeyPath
|
|
)
|
|
|
|
$tempDir = Join-Path $env:TEMP 'aw-rus-1c-ssh'
|
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
$tempKeyPath = Join-Path $tempDir 'awops_ed25519'
|
|
Copy-Item -LiteralPath $SourceKeyPath -Destination $tempKeyPath -Force
|
|
|
|
& icacls.exe $tempKeyPath /inheritance:r | Out-Null
|
|
& icacls.exe $tempKeyPath /grant:r "$($env:USERNAME):(F)" | Out-Null
|
|
& icacls.exe $tempKeyPath /remove:g 'Users' 'Authenticated Users' 'Everyone' 'BUILTIN\Users' 'BUILTIN\Administrators' 'NT AUTHORITY\SYSTEM' 2>$null | Out-Null
|
|
|
|
return $tempKeyPath
|
|
}
|
|
|
|
function Get-1CFileInfobases {
|
|
$results = New-Object System.Collections.Generic.List[object]
|
|
$launcherFiles = Get-ChildItem -Path 'C:\Users' -Directory -ErrorAction SilentlyContinue |
|
|
ForEach-Object { Join-Path $_.FullName 'AppData\Roaming\1C\1CEStart\ibases.v8i' } |
|
|
Where-Object { Test-Path -LiteralPath $_ }
|
|
|
|
foreach ($file in $launcherFiles) {
|
|
$userName = Split-Path -Leaf (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $file))))
|
|
$currentName = $null
|
|
$currentId = $null
|
|
foreach ($lineRaw in Get-Content -LiteralPath $file -Encoding UTF8) {
|
|
$line = [string]$lineRaw
|
|
if ($line -match '^\[(.+)\]$') {
|
|
$currentName = $Matches[1]
|
|
$currentId = $null
|
|
continue
|
|
}
|
|
if ($line -match '^ID=(.+)$') {
|
|
$currentId = $Matches[1].Trim()
|
|
continue
|
|
}
|
|
if ($line -match '^Connect=File="(.+)";$' -and $currentName) {
|
|
$results.Add([pscustomobject]@{
|
|
userName = $userName
|
|
infobase = $currentName
|
|
baseId = $currentId
|
|
path = $Matches[1]
|
|
launcherFile = $file
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return $results |
|
|
Group-Object infobase, path |
|
|
ForEach-Object { $_.Group | Select-Object -First 1 }
|
|
}
|
|
|
|
function Get-HostSample {
|
|
$os = Get-CimInstance Win32_OperatingSystem
|
|
$cpuSample = Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue |
|
|
Measure-Object -Property LoadPercentage -Average
|
|
$cpu = if ($cpuSample.Count -gt 0 -and $null -ne $cpuSample.Average) { [double]$cpuSample.Average } else { 0 }
|
|
$disk = Get-PSDrive -Name E -ErrorAction SilentlyContinue
|
|
$rdp = (quser 2>$null | Select-Object -Skip 1 | Measure-Object).Count
|
|
|
|
return [ordered]@{
|
|
ts = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
host = $env:COMPUTERNAME
|
|
cpu_pct = [math]::Round($cpu, 2)
|
|
ram_pct = [math]::Round((($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize) * 100, 2)
|
|
disk_free_gb = if ($disk) { [math]::Round($disk.Free / 1GB, 2) } else { 0 }
|
|
disk_latency_ms = 0
|
|
smb_errors = 0
|
|
rdp_sessions = $rdp
|
|
backup_ok = 1
|
|
}
|
|
}
|
|
|
|
function Write-JsonLines {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[Parameter(Mandatory = $true)]
|
|
[object]$Rows
|
|
)
|
|
|
|
$directory = Split-Path -Parent $Path
|
|
if ($directory) {
|
|
New-Item -ItemType Directory -Path $directory -Force | Out-Null
|
|
}
|
|
$normalizedRows = @()
|
|
if ($null -ne $Rows) {
|
|
$normalizedRows = @($Rows)
|
|
}
|
|
$normalizedRows |
|
|
ForEach-Object { $_ | ConvertTo-Json -Depth 8 -Compress } |
|
|
Set-Content -LiteralPath $Path -Encoding UTF8
|
|
}
|
|
|
|
function Invoke-SshUploadWithRetry {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$KeyPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$SourcePath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Destination,
|
|
[int]$Attempts = 3,
|
|
[int]$DelaySeconds = 5
|
|
)
|
|
|
|
for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
|
|
Write-RunLog "scp attempt=$attempt source=$SourcePath destination=$Destination"
|
|
& $ScpExe -q -i $KeyPath -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL $SourcePath $Destination
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-RunLog "scp success source=$SourcePath"
|
|
return
|
|
}
|
|
if ($attempt -ge $Attempts) {
|
|
throw "scp upload failed after $Attempts attempts for $SourcePath with rc=$LASTEXITCODE"
|
|
}
|
|
Write-RunLog "scp retry source=$SourcePath rc=$LASTEXITCODE delay=${DelaySeconds}s"
|
|
Start-Sleep -Seconds $DelaySeconds
|
|
}
|
|
}
|
|
|
|
function Get-ExporterState {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
return @{}
|
|
}
|
|
|
|
$raw = Get-Content -Raw -LiteralPath $Path -Encoding UTF8
|
|
if ([string]::IsNullOrWhiteSpace($raw)) {
|
|
return @{}
|
|
}
|
|
|
|
$payload = $raw | ConvertFrom-Json
|
|
$state = @{}
|
|
foreach ($prop in $payload.PSObject.Properties) {
|
|
$state[[string]$prop.Name] = $prop.Value
|
|
}
|
|
return $state
|
|
}
|
|
|
|
function Save-ExporterState {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable]$State
|
|
)
|
|
|
|
$directory = Split-Path -Parent $Path
|
|
if ($directory) {
|
|
New-Item -ItemType Directory -Path $directory -Force | Out-Null
|
|
}
|
|
$State | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $Path -Encoding UTF8
|
|
}
|
|
|
|
function Get-CompanyActivityScore {
|
|
param(
|
|
[double]$DbDeltaMb,
|
|
[double]$ReglogDeltaMb,
|
|
[int]$ActiveLocks,
|
|
[bool]$HasTempDb,
|
|
[bool]$SchedulerTouched,
|
|
[string]$Status,
|
|
[bool]$IsBootstrap
|
|
)
|
|
|
|
$score = 0.0
|
|
$score += [math]::Abs($DbDeltaMb)
|
|
$score += [math]::Abs($ReglogDeltaMb)
|
|
if ($ActiveLocks -gt 0) {
|
|
$score += ($ActiveLocks * 5)
|
|
}
|
|
if ($HasTempDb) {
|
|
$score += 10
|
|
}
|
|
if ($SchedulerTouched) {
|
|
$score += 3
|
|
}
|
|
if ($Status -eq 'busy') {
|
|
$score += 5
|
|
}
|
|
if ($IsBootstrap -and $score -le 0) {
|
|
$score = 1
|
|
}
|
|
return [math]::Round($score, 2)
|
|
}
|
|
|
|
Write-RunLog 'file1c exporter start'
|
|
|
|
if (-not (Test-Path -LiteralPath $ScpExe)) {
|
|
throw "scp client not found: $ScpExe"
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $RemoteKeyPath)) {
|
|
throw "SSH private key not found: $RemoteKeyPath"
|
|
}
|
|
|
|
$config = Get-Content -Raw -LiteralPath $ConfigPath | ConvertFrom-Json
|
|
if ([string]::IsNullOrWhiteSpace($AnalyticsHost)) {
|
|
if ($config.PSObject.Properties.Name -contains 'analytics' -and
|
|
$config.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and
|
|
$config.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetHost') {
|
|
$AnalyticsHost = [string]$config.analytics.file1cAutomation.targetHost
|
|
}
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($AnalyticsHost)) {
|
|
throw "AnalyticsHost is empty and deployment-config has no analytics.file1cAutomation.targetHost"
|
|
}
|
|
if ($config.PSObject.Properties.Name -contains 'analytics' -and
|
|
$config.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and
|
|
$config.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetUser' -and
|
|
-not [string]::IsNullOrWhiteSpace([string]$config.analytics.file1cAutomation.targetUser)) {
|
|
$AnalyticsUser = [string]$config.analytics.file1cAutomation.targetUser
|
|
}
|
|
if ($config.PSObject.Properties.Name -contains 'analytics' -and
|
|
$config.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and
|
|
$config.analytics.file1cAutomation.PSObject.Properties.Name -contains 'remoteRoot' -and
|
|
-not [string]::IsNullOrWhiteSpace([string]$config.analytics.file1cAutomation.remoteRoot)) {
|
|
$RemoteRoot = [string]$config.analytics.file1cAutomation.remoteRoot
|
|
}
|
|
|
|
$infobases = @(Get-1CFileInfobases)
|
|
$nowUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
$exporterState = Get-ExporterState -Path $ExporterStatePath
|
|
$nextExporterState = @{}
|
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
|
|
$documents = New-Object System.Collections.Generic.List[object]
|
|
$reglog = New-Object System.Collections.Generic.List[object]
|
|
$audit = New-Object System.Collections.Generic.List[object]
|
|
|
|
foreach ($base in $infobases) {
|
|
$dbFile = Join-Path $base.path '1Cv8.1CD'
|
|
$dbItem = Get-Item -LiteralPath $dbFile -ErrorAction SilentlyContinue
|
|
$logDir = Join-Path $base.path '1Cv8Log'
|
|
$logItems = @(Get-ChildItem -LiteralPath $logDir -File -ErrorAction SilentlyContinue)
|
|
$mainLog = $logItems | Where-Object { $_.Extension -ieq '.lgp' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
$activeLocks = @(Get-ChildItem -LiteralPath $base.path -File -Filter '1Cv8*.1CL*' -ErrorAction SilentlyContinue)
|
|
$tempDb = Get-Item -LiteralPath (Join-Path $base.path '1Cv8tmp.1CD') -ErrorAction SilentlyContinue
|
|
$schedulerDir = Get-Item -LiteralPath (Join-Path $base.path '1Cv8JobScheduler') -ErrorAction SilentlyContinue
|
|
$owner = if ($base.userName) { [string]$base.userName } else { 'unknown' }
|
|
$organization = [string](Split-Path -Leaf (Split-Path -Parent $base.path))
|
|
$status = if ($activeLocks.Count -gt 0 -or $tempDb) { 'busy' } else { 'online' }
|
|
$docId = if ($base.baseId) { [string]$base.baseId } else { ([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$base.path)).TrimEnd('=').Replace('/','_').Replace('+','-')) }
|
|
$stateKey = [string]$docId
|
|
$previous = if ($exporterState.ContainsKey($stateKey)) { $exporterState[$stateKey] } else { $null }
|
|
$dbSizeBytes = if ($dbItem) { [int64]$dbItem.Length } else { 0 }
|
|
$mainLogBytes = if ($mainLog) { [int64]$mainLog.Length } else { 0 }
|
|
$schedulerWriteUtc = if ($schedulerDir) { [datetime]$schedulerDir.LastWriteTimeUtc } else { $null }
|
|
$dbDeltaMb = 0.0
|
|
$reglogDeltaMb = 0.0
|
|
$schedulerTouched = $false
|
|
$isBootstrap = ($null -eq $previous)
|
|
|
|
if (-not $isBootstrap) {
|
|
$prevDbSize = if ($previous.PSObject.Properties.Name -contains 'dbSizeBytes') { [double]$previous.dbSizeBytes } else { 0 }
|
|
$prevMainLogSize = if ($previous.PSObject.Properties.Name -contains 'mainLogBytes') { [double]$previous.mainLogBytes } else { 0 }
|
|
$dbDeltaMb = [math]::Round(($dbSizeBytes - $prevDbSize) / 1MB, 2)
|
|
$reglogDeltaMb = [math]::Round(($mainLogBytes - $prevMainLogSize) / 1MB, 2)
|
|
if ($schedulerWriteUtc -and $previous.PSObject.Properties.Name -contains 'schedulerWriteUtc' -and -not [string]::IsNullOrWhiteSpace([string]$previous.schedulerWriteUtc)) {
|
|
$prevSchedulerWriteUtc = [datetime]::Parse([string]$previous.schedulerWriteUtc)
|
|
$schedulerTouched = ($schedulerWriteUtc -gt $prevSchedulerWriteUtc)
|
|
}
|
|
elseif ($schedulerWriteUtc) {
|
|
$schedulerTouched = $true
|
|
}
|
|
}
|
|
|
|
$activityScore = Get-CompanyActivityScore `
|
|
-DbDeltaMb $dbDeltaMb `
|
|
-ReglogDeltaMb $reglogDeltaMb `
|
|
-ActiveLocks $activeLocks.Count `
|
|
-HasTempDb ([bool]$tempDb) `
|
|
-SchedulerTouched $schedulerTouched `
|
|
-Status $status `
|
|
-IsBootstrap $isBootstrap
|
|
|
|
$documents.Add([ordered]@{
|
|
ts = $nowUtc
|
|
infobase = [string]$base.infobase
|
|
organization = $organization
|
|
department = 'FileBase'
|
|
doc_type = 'InfobaseSnapshot'
|
|
doc_id = $docId
|
|
doc_number = ''
|
|
author = $owner
|
|
counterparty = ''
|
|
operation_type = 'inventory'
|
|
amount = 0
|
|
status = $status
|
|
posted = 1
|
|
})
|
|
|
|
if ($activityScore -gt 0) {
|
|
$documents.Add([ordered]@{
|
|
ts = $nowUtc
|
|
infobase = [string]$base.infobase
|
|
organization = $organization
|
|
department = 'FileBaseActivity'
|
|
doc_type = 'CompanyActivitySnapshot'
|
|
doc_id = "$docId-$stamp"
|
|
doc_number = $stamp
|
|
author = $owner
|
|
counterparty = [string]$base.infobase
|
|
operation_type = 'activity_snapshot'
|
|
amount = $activityScore
|
|
status = $status
|
|
posted = 1
|
|
})
|
|
}
|
|
|
|
$audit.Add([ordered]@{
|
|
ts = $nowUtc
|
|
infobase = [string]$base.infobase
|
|
user = $owner
|
|
object_type = 'infobase'
|
|
object_id = $docId
|
|
action = 'inventory_snapshot'
|
|
before_hash = ''
|
|
after_hash = ''
|
|
risk_tag = if ($status -eq 'busy') { 'busy' } else { '' }
|
|
})
|
|
|
|
if ($mainLog) {
|
|
$reglog.Add([ordered]@{
|
|
ts = ([datetime]$mainLog.LastWriteTimeUtc).ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
infobase = [string]$base.infobase
|
|
user = $owner
|
|
host = $env:COMPUTERNAME
|
|
app = '1cv8-file'
|
|
event_name = 'RegLogInventory'
|
|
level = if ($mainLog.Length -gt 536870912) { 'warn' } else { 'info' }
|
|
duration_ms = 0
|
|
message = "Registration log file $($mainLog.Name) size=$([math]::Round($mainLog.Length / 1MB, 2))MB path=$($mainLog.FullName)"
|
|
})
|
|
}
|
|
|
|
if ($activeLocks.Count -gt 0 -or $tempDb) {
|
|
$reglog.Add([ordered]@{
|
|
ts = $nowUtc
|
|
infobase = [string]$base.infobase
|
|
user = $owner
|
|
host = $env:COMPUTERNAME
|
|
app = '1cv8-file'
|
|
event_name = 'FileBaseBusy'
|
|
level = 'warn'
|
|
duration_ms = 0
|
|
message = "Detected active file-base markers: locks=$($activeLocks.Count) tempDb=$([bool]$tempDb)"
|
|
})
|
|
}
|
|
|
|
if ($schedulerDir) {
|
|
$reglog.Add([ordered]@{
|
|
ts = ([datetime]$schedulerDir.LastWriteTimeUtc).ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
infobase = [string]$base.infobase
|
|
user = $owner
|
|
host = $env:COMPUTERNAME
|
|
app = '1cv8-file'
|
|
event_name = 'JobSchedulerActivity'
|
|
level = 'info'
|
|
duration_ms = 0
|
|
message = "1Cv8JobScheduler touched at $([datetime]$schedulerDir.LastWriteTimeUtc)"
|
|
})
|
|
}
|
|
|
|
$reglog.Add([ordered]@{
|
|
ts = $nowUtc
|
|
infobase = [string]$base.infobase
|
|
user = $owner
|
|
host = $env:COMPUTERNAME
|
|
app = '1cv8-file'
|
|
event_name = 'CompanyActivitySnapshot'
|
|
level = if ($activityScore -gt 20) { 'warn' } else { 'info' }
|
|
duration_ms = 0
|
|
message = "activityScore=$activityScore dbDeltaMb=$dbDeltaMb reglogDeltaMb=$reglogDeltaMb locks=$($activeLocks.Count) tempDb=$([bool]$tempDb) schedulerTouched=$schedulerTouched"
|
|
})
|
|
|
|
$nextExporterState[$stateKey] = [ordered]@{
|
|
infobase = [string]$base.infobase
|
|
path = [string]$base.path
|
|
dbSizeBytes = $dbSizeBytes
|
|
mainLogBytes = $mainLogBytes
|
|
schedulerWriteUtc = if ($schedulerWriteUtc) { $schedulerWriteUtc.ToString('o') } else { '' }
|
|
updatedAtUtc = $nowUtc
|
|
}
|
|
}
|
|
|
|
$hostRows = @((Get-HostSample))
|
|
|
|
$outRoot = Join-Path $env:TEMP "aw-rus-1c-outbox-$stamp"
|
|
New-Item -ItemType Directory -Path $outRoot -Force | Out-Null
|
|
|
|
$files = @{
|
|
documents = Join-Path $outRoot "documents-$stamp.jsonl"
|
|
reglog = Join-Path $outRoot "reglog-$stamp.jsonl"
|
|
audit = Join-Path $outRoot "audit-$stamp.jsonl"
|
|
host = Join-Path $outRoot "host-$stamp.jsonl"
|
|
}
|
|
|
|
$documentRows = @($documents | ForEach-Object { $_ })
|
|
$reglogRows = @($reglog | ForEach-Object { $_ })
|
|
$auditRows = @($audit | ForEach-Object { $_ })
|
|
$hostRowsNormalized = @($hostRows | ForEach-Object { $_ })
|
|
|
|
Write-JsonLines -Path ([string]$files['documents']) -Rows $documentRows
|
|
Write-JsonLines -Path ([string]$files['reglog']) -Rows $reglogRows
|
|
Write-JsonLines -Path ([string]$files['audit']) -Rows $auditRows
|
|
Write-JsonLines -Path ([string]$files['host']) -Rows $hostRowsNormalized
|
|
|
|
$effectiveKeyPath = New-TemporarySshKeyCopy -SourceKeyPath $RemoteKeyPath
|
|
|
|
try {
|
|
Write-RunLog "prepared datasets documents=$($documentRows.Count) reglog=$($reglogRows.Count) audit=$($auditRows.Count) host=$($hostRowsNormalized.Count)"
|
|
foreach ($dataset in 'documents', 'reglog', 'audit', 'host') {
|
|
Invoke-SshUploadWithRetry -KeyPath $effectiveKeyPath -SourcePath ([string]$files[$dataset]) -Destination "$AnalyticsUser@$AnalyticsHost`:$RemoteRoot/$dataset/"
|
|
}
|
|
Save-ExporterState -Path $ExporterStatePath -State $nextExporterState
|
|
Write-RunLog "upload complete analyticsHost=$AnalyticsHost remoteRoot=$RemoteRoot"
|
|
}
|
|
finally {
|
|
Remove-Item -LiteralPath $effectiveKeyPath -Force -ErrorAction SilentlyContinue
|
|
Remove-Item -LiteralPath $outRoot -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Write-RunLog 'file1c exporter done'
|
|
|
|
[ordered]@{
|
|
analyticsHost = $AnalyticsHost
|
|
analyticsUser = $AnalyticsUser
|
|
remoteRoot = $RemoteRoot
|
|
infobases = @($infobases | ForEach-Object { $_.infobase })
|
|
datasets = [ordered]@{
|
|
documents = $documents.Count
|
|
reglog = $reglog.Count
|
|
audit = $audit.Count
|
|
host = $hostRows.Count
|
|
}
|
|
} | ConvertTo-Json -Depth 8
|