Automação do servidor Jenkins SQL: retornando o resultado lindamente

Continuando o tema de organizar o Zero Touch PROD no RDS novamente. Os futuros DBAs não poderão se conectar aos servidores PROD diretamente, mas poderão usar os trabalhos Jenkins para um conjunto limitado de operações. O DBA inicia o trabalho e, após algum tempo, recebe uma carta com um relatório sobre a conclusão desta operação. Vejamos maneiras de apresentar esses resultados ao usuário.



Texto simples


Vamos começar com o mais trivial. O primeiro método é tão simples que não há o que falar em geral (o autor a seguir usa os trabalhos do FreeStyle):



O sqlcmd faz alguma coisa e nós a apresentamos ao usuário. Ideal para, por exemplo, tarefas de backup:



A propósito, não esquecemos que, no RDS, o backup / restauração é assíncrono; portanto, devemos esperar por isso:

declare @rds table (id int, task_type varchar(128), database_name sysname, pct int, duration int, lifecycle varchar(128), taskinfo varchar(max) null, upd datetime, cre datetime, s3 varchar(256), ovr int, KMS varchar(256) null) waitfor delay '00:00:20' insert into @rds exec msdb.dbo.rds_task_status @db_name='{db}' select @xid=max(id) from @rds again: waitfor delay '00:00:02' delete from @rds insert into @rds exec msdb.dbo.rds_task_status @db_name='{db}' # {db} substituted with db name by powershell select @stat=lifecycle,@info=taskinfo from @rds where id=@xid if @stat not in ('ERROR','SUCCESS','CANCELLED') goto again 

A segunda maneira, CSV


Tudo aqui também é muito simples:



No entanto, esse método funciona apenas se os dados retornados ao CSV forem "simples". Se você tentar retornar, por exemplo, a lista de consultas intensivas da CPU N TOP, o CSV "corroerá" devido ao fato de o texto da consulta poder conter caracteres - vírgulas, aspas e até quebras de linha. Portanto, precisamos de algo mais complicado.

Lindas tags HTML


Vou lhe dar um trecho de código imediatamente

 $Header = @" <style> TABLE {border-width: 1px; border-style: solid; border-color: black; border-collapse: collapse;} TH {border-width: 1px; padding: 3px; border-style: solid; border-color: black; background-color: #6495ED;} TD {border-width: 1px; padding: 3px; border-style: solid; border-color: black;} </style> "@ $Result = invoke-Sqlcmd -ConnectionString $jstr -Query "select * from DbInv" ` | Select-Object -Property * -ExcludeProperty "ItemArray", "RowError", "RowState", "Table", "HasErrors" if ($Result -eq $null) { $cnt = 0; } elseif ($Result.getType().FullName -eq "System.Management.Automation.PSCustomObject") { $cnt = 1; } else { $cnt = $Result.Rows.Count; } if ($cnt -gt 0) { $body = "<h2>My table</h2>" $Result | ConvertTo-HTML -Title "Rows" -Head $header -body $body ` | Out-File "res.log" -Append -Encoding UTF8 } else { "<h3>No data</h3>" | Out-File "res.log" -Append -Encoding UTF8 } 

A propósito, preste atenção à linha com System.Management.Automation.PSCustomObject, é mágico, se houver exatamente uma linha na grade, ocorreram alguns problemas. A decisão foi tomada na Internet sem muita compreensão. Como resultado, você obtém a saída, escrita algo como isto:



Desenhar gráficos


Atenção: código pervertido abaixo!

Há uma pergunta engraçada ao servidor SQL que exibe a CPU nos últimos N minutos - acontece que o camarada Major se lembra de tudo! Tente este:

 DECLARE @ts_now bigint = (SELECT cpu_ticks/(cpu_ticks/ms_ticks) FROM sys.dm_os_sys_info WITH (NOLOCK)); SELECT TOP(256) DATEADD(ms, -1 * (@ts_now - [timestamp]), GETDATE()) AS [EventTime], SQLProcessUtilization AS [SQLCPU], 100 - SystemIdle - SQLProcessUtilization AS [OtherCPU] FROM (SELECT record.value('(./Record/@id)[1]', 'int') AS record_id, record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS [SystemIdle], record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS [SQLProcessUtilization], [timestamp] FROM (SELECT [timestamp], CONVERT(xml, record) AS [record] FROM sys.dm_os_ring_buffers WITH (NOLOCK) WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE N'%<SystemHealth>%') AS x) AS y ORDER BY 1 DESC OPTION (RECOMPILE); 

Agora, usando esta formatação (variável $ Fragment)
 <table style="width: 100%"><tbody><tr style="background-color: white; height: 2pt;"> <td style="width: SQLCPU%; background-color: green;"></td> <td style="width: OtherCPU%; background-color: blue;"></td> <td style="width: REST%; background-color: #C0C0C0;"></td></tr></tbody> </table> 

Podemos formar o corpo da carta:
 $Result = invoke-Sqlcmd -ConnectionString $connstr -Query $Query ` | Select-Object -Property * -ExcludeProperty ` "ItemArray", "RowError", "RowState", "Table", "HasErrors" if ($Result.HasRows) { foreach($item in $Result) { $time = $itemEventTime $sqlcpu = $item.SQLCPU $other = $itemOtherCPU $rest = 100 - $sqlcpu - $other $f = $fragment -replace "SQLCPU", $sqlcpu $f = $f -replace "OtherCPU", $other $f = $f -replace "REST", $rest $f | Out-File "res.log" -Append -Encoding UTF8 } 

O que ficaria assim:



Sim, Monsieur sabe muito sobre perversões! Curiosamente, esse código contém: PowerShell (escrito nele), SQL, Xquery, HTML. É uma pena que não possamos adicionar Javascript ao HTML (já que isso é para escrever), mas é dever de todos criar um código Python (que pode ser usado no SQL)!

Saída de Rastreio do Profiler SQL


É claro que o rastreio não se encaixará no CSV devido ao campo TextData. Mas também é estranho exibir a grade em uma carta, tanto por causa do tamanho quanto porque esses dados costumam ser usados ​​para análises posteriores. Portanto, fazemos o seguinte: invocamos através de invoke-SqlCmd um determinado script, nas entranhas das quais é feito

 select SPID,EventClass,TextData, Duration,Reads,Writes,CPU, StartTime,EndTime,DatabaseName,HostName, ApplicationName,LoginName from ::fn_trace_gettable ( @filename , default ) 

Além disso, em outro servidor acessível pelo DBA, há uma base Traces com um espaço em branco vazio, uma placa Modelo, pronta para aceitar todas as colunas especificadas. Copiamos esse modelo para uma nova tabela com um nome exclusivo:

 $dt = Get-Date -format "yyyyMMdd" $tm = Get-Date -format "hhmmss" $tableName = $srv + "_" + $dt + "_" + $tm $copytab = "select * into " + $tableName + " from Model" invoke-SqlCmd -ConnectionString $tstr -Query $copytab 

E agora podemos escrever nosso rastreamento usando Data.SqlClient.SqlBulkCopy - eu já dei um exemplo disso acima. Sim, seria bom mascarar constantes no TextData:

 # mask data foreach ($Row in $Result) { $v = $Row["TextData"] $v = $v -replace "'([^']{2,})'", "'str'" -replace "[0-9][0-9]+", '999' $Row["TextData"] = $v } 

Substituímos números por mais de um caractere com um comprimento de 999 e substituímos cadeias de caracteres maiores que um caractere por 'str'. Os números de 0 a 9 são frequentemente usados ​​como sinalizadores, e não os tocamos, nem como cadeias de caracteres vazias e de caractere único - entre eles, freqüentemente são encontrados 'Y', 'N' etc.

Adicione cores às nossas vidas (com mais de 18 anos)


Nos comprimidos, você geralmente deseja destacar as células que requerem atenção. Por exemplo, FAILS, um alto nível de fragmentação etc. Obviamente, isso também pode ser feito em SQL simples, formando HTML usando PRINT, e no Jenkins você pode definir o tipo de arquivo HTML:

 declare @body varchar(max), @chunk varchar(max) set @body='<font face="Lucida Console" size="3">' set @body=@body+'<b>Server name: '+@@servername+'</b><br>' set @body=@body+'<br><br>' set @body=@body+'<table><tr><th>Job</th><th>Last Run</th><th>Avg Duration, sec</th><th>Last Run, Sec</th><th>Last Status</th></tr>' print @body DECLARE tab CURSOR FOR SELECT '<tr><td>'+name+'</td><td>'+ LastRun+'</td><td>'+ convert(varchar,AvgDuration)+'</td><td>'+ convert(varchar,LastDuration)+'</td><td>'+ case when LastStatus<>'Succeeded' then '<font color="red">' else '' end+ LastStatus+ case when LastStatus<>'Succeeded' then '</font>' else '' end+ +'</td><td>' from #j2 OPEN tab; FETCH NEXT FROM tab into @chunk WHILE @@FETCH_STATUS = 0 BEGIN print @chunk FETCH NEXT FROM tab into @chunk; END CLOSE tab; DEALLOCATE tab; print '</table>' 

Por que eu escrevi esse código?



Mas há uma solução mais bonita. ConvertTo-HTML não nos permite colorir as células, mas podemos fazê-lo após o fato. Por exemplo, queremos selecionar células com um nível de fragmentação superior a 80 e superior a 90. Adicione estilos:

 <style> .SQLmarkup-red { color: red; background-color: yellow; } .SQLmarkup-yellow { color: black; background-color: #FFFFE0; } .SQLmarkup-default { color: black; background-color: white; } </style> 

No próprio qi, adicionaremos uma coluna fictícia diretamente à coluna que queremos colorir. A coluna deve ser chamada SQLmarkup- something:
 case when ps.avg_fragmentation_in_percent>=90.0 then 'SQLmarkup-red' when ps.avg_fragmentation_in_percent>=80.0 then 'SQLmarkup-yellow' else 'SQLmarkup-default' end as [SQLmarkup-1], ps.avg_fragmentation_in_percent, 

Agora, tendo recebido o HTML criado pelo Powershell, removeremos a coluna dummy do cabeçalho e transferiremos o valor da coluna para o estilo no corpo dos dados. Isso é feito em apenas duas permutações:

 $html = $html ` -replace "<th>SQLmarkup[^<]*</th>", "" ` -replace "<td>SQLmarkup-(.+?)</td><td>",'<td class="SQLmarkup-$1">' 

Resultado:



Não é elegante? Embora não, algo que esta coloração me lembra

Source: https://habr.com/ru/post/pt447242/


All Articles