这一篇还是承接之前的思路,在上一篇文章中,我们分享了一个思路,可以把storage account的访问日志,通过PowerShell脚本下载下来,然后自动上传到Log Analytics,这样就可以直接在LA中对日志进行分析和查询了,这种方法虽然简单方便,但终究算不上是自动化的方式,扩展一下思路之后,我们不妨尝试着用eventgrid结合function的方式,把这套方法转换成一套自动化的方案

首先先把EventGrid和Function是什么普及下

EventGrid

通过 Azure 事件网格,可使用基于事件的体系结构轻松生成应用程序。 首先,选择要订阅的 Azure 资源,然后提供要向其发送事件的事件处理程序或 WebHook 终结点。 事件网格包含来自 Azure 服务对事件的内置支持,如存储 blob 和资源组。 事件网格还使用自定义主题支持自己的事件。

简单来说就像是个触发器一样,可以触发各种事件,然后做出针对性的响应,听起来和logic apps里的触发器有点像,但eventgrid只是个单纯的事件中转工具,下游的处理方式完全由其他产品完成,以下这张图也可以看出来eventgrid所处的位置,类似一个消息队列一样用来做事件的传输

https://docs.microsoft.com/en-us/azure/event-grid/?WT.mc_id=AZ-MVP-5001235

图片1.png

Function

Azure Functions 是一种无服务器解决方案,可以使用户减少代码编写、减少需要维护的基础结构并节省成本。 无需担心部署和维护服务器,云基础结构提供保持应用程序运行所需的所有最新资源。

Function其实比较好理解,现在各个云基本上都有类似的产品,横向对比的话就是AWS的lambda,一个完全托管的代码运行平台

https://docs.microsoft.com/zh-cn/azure/azure-functions/functions-overview?WT.mc_id=AZ-MVP-5001235

结合上边的图,其实就能看出来我们的思路,eventgird内置了blob的触发器,也就是说当新的blob出现时,就会自动触发eventgrid,而下游的处理程序,我们就可以结合function来做,代码其实是现成的,就用之前的就可以,只不过需要稍加改动而已,总体的工作量很小

当然这里其实有个隐藏的问题,因为storage account的log是存储在$logs容器里,而这个容器在Azure后台是不会触发eventgrid的,这就有点坑了,我们采用的方法是可以创建个function,然后用azcopy定期的把log sync到另外一个container即可,一行代码就搞定,很简单,所以暂时先不写出来了

实现步骤

下边就来看具体的实现步骤了,首先先创建好function app,function app和function的关系很简单,function app相当于是运行function的平台,代码是跑在这个平台上的,function app中可以包含很多个function

创建Function App

function app创建过程非常简单,我们选择runtime是PowerShell Core即可

图片2.png

创建Function 

Function app创建好之后,就可以在里边创建function了,azure其实内置了evenr grid trigger的function,创建时直接选择即可

图片4.png


微信截图_20210112104638.png


创建Event Grid Subscription

function准备好了,接下来就可以准备eventgrid了,可以直接在function里创建event grid subscription

图片5.png

创建event grid subscription时,可以选择的type有很多,这里注意要选择storage类型的

图片6.png


配置Storage account的event type

图片7.png


然后注意需要配置一下filter,因为我们要把路径限制到某个特定的范围,而不是所有blob都会触发事件

图片8.png

触发器也准备完成了,接下来就可以准备在function里处理的代码了

编写Code

因为之前的代码是循环处理的,而eventgrid实际上是一条条推送过来的,所以这里的逻辑需要进行些许调整

Function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource)
{
    $xHeaders = "x-ms-date:" + $date
    $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource

    $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)
    $keyBytes = [Convert]::FromBase64String($sharedKey)

    $sha256 = New-Object System.Security.Cryptography.HMACSHA256
    $sha256.Key = $keyBytes
    $calculatedHash = $sha256.ComputeHash($bytesToHash)
    $encodedHash = [Convert]::ToBase64String($calculatedHash)
    $authorization = 'SharedKey {0}:{1}' -f $customerId,$encodedHash
    return $authorization
}

Function Post-LogAnalyticsData($customerId, $sharedKey, $body, $logType)
{

    

    $method = "POST"
    $contentType = "application/json"
    $resource = "/api/logs"
    $rfc1123date = [DateTime]::UtcNow.ToString("r")
    $contentLength = $body.Length
    $signature = Build-Signature `
        -customerId $customerId `
        -sharedKey $sharedKey `
        -date $rfc1123date `
        -contentLength $contentLength `
        -method $method `
        -contentType $contentType `
        -resource $resource
    $uri = "https://" + $customerId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01"

    $headers = @{
        "Authorization" = $signature;
        "Log-Type" = $logType;
        "x-ms-date" = $rfc1123date;
        "time-generated-field" = $TimeStampField;
    }

    $response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $body -UseBasicParsing
    return $response.StatusCode
}

Function ConvertSemicolonToURLEncoding([String] $InputText)
{
    $ReturnText = ""
    $chars = $InputText.ToCharArray()



    $StartConvert = $false



    foreach($c in $chars)
    {
        if($c -eq '"') {
            $StartConvert = ! $StartConvert
        }


        if($StartConvert -eq $true -and $c -eq ';')
        {
            $ReturnText += "%3B"
        } else {
            $ReturnText += $c
        }
    }

    return $ReturnText
}

Function FormalizeJsonValue($Text)
{
    $Text1 = ""
    if($Text.IndexOf("`"") -eq 0) { $Text1=$Text } else {$Text1="`"" + $Text+ "`""}
    if($Text1.IndexOf("%3B") -ge 0) {
        $ReturnText = $Text1.Replace("%3B", ";")
    } else {
        $ReturnText = $Text1
    }
    return $ReturnText
}

Function ConvertLogLineToJson([String] $logLine)
{


    $logLineEncoded = ConvertSemicolonToURLEncoding($logLine)

    $elements = $logLineEncoded.split(';')

    

    $FormattedElements = New-Object System.Collections.ArrayList
                
    foreach($element in $elements)
    {

        $NewText = FormalizeJsonValue($element)
        $FormattedElements.Add($NewText) > null
    }
    $Columns = 
    (   "version-number",
        "request-start-time",
        "operation-type",
        "request-status",
        "http-status-code",
        "end-to-end-latency-in-ms",
        "server-latency-in-ms",
        "authentication-type",
        "requester-account-name",
        "owner-account-name",
        "service-type",
        "request-url",
        "requested-object-key",
        "request-id-header",
        "operation-count",
        "requester-ip-address",
        "request-version-header",
        "request-header-size",
        "request-packet-size",
        "response-header-size",
        "response-packet-size",
        "request-content-length",
        "request-md5",
        "server-md5",
        "etag-identifier",
        "last-modified-time",
        "conditions-used",
        "user-agent-header",
        "referrer-header",
        "client-request-id"
    )

    $logJson = "[{";
    For($i = 0;$i -lt $Columns.Length;$i++)
    {
        $logJson += "`"" + $Columns[$i] + "`":" + $FormattedElements[$i]
        if($i -lt $Columns.Length - 1) {
            $logJson += ","
        }
    }
    $logJson += "}]";

    return $logJson
}

$storageAccount = Get-AzStorageAccount -ResourceGroupName $ResourceGroup -Name $StorageAccountName -ErrorAction SilentlyContinue
if($null -eq $storageAccount)
{
    throw "The storage account specified does not exist in this subscription."
}

$storageContext = $storageAccount.Context


$token = $Null
$maxReturn = 5000
$successPost = 0
$failedPost = 0

$subject=$eventGridEvent.subject.ToString()
$BlobArray=$subject.Split('/')
$container=$BlobArray[$BlobArray.indexof('containers')+1]
$BlobIndex=$subject.indexof('blobs/')+6
$Blob=$subject.substring($BlobIndex,$subject.length - $BlobIndex)

Write-Output("> Downloading blob: {0}" -f $blob)
$filename = ".\log.txt"
Get-AzStorageBlobContent -Context $storageContext -Container $container -Blob $blob -Destination $filename -Force > Null
Write-Output("> Posting logs to log analytic workspace: {0}" -f $blob)

$lines = Get-Content $filename

foreach($line in $lines)
        {
            $json = ConvertLogLineToJson($line)
           

            $response = Post-LogAnalyticsData -customerId $customerId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($json)) -logType $logType

            if($response -eq "200") {
                $successPost++
            } else { 
                $failedPost++
                Write-Output "> Failed to post one log to Log Analytics workspace"
            }
        }

        remove-item $filename -Force

    


Write-Output "> Log lines posted to Log Analytics workspace: success = $successPost, failure = $failedPost"


最后还有一个步骤是需要给function app授权来访问storage,这步就不详细讲了,也可以用storage account key来做,当然这种方法其实不是很推荐

最后可以在function里的监控里看到完成的效果

图片10.png

总结

总体来说,其实和单纯通过PowerShell脚本的方式相比变化并不大,但是因为增加了eventgrid和function,让整个方案变得更加灵活,类似的思路还可以扩展到很多其他任务上,以更cloud native的方式来看待和处理问题