Can I send my Octopus Deploy task logs to an external log analyzer?

Note: You can send your Octopus Server logs to an external logger; the details are here. This article refers to task logs specifically.

To send your deployment or runbook task logs from Octopus, you need to call the Octopus API. This can be done in a script step as the last step of your process so that the task logs for all previous steps are available.

Let’s take an example of sending task log information to Datadog; writing a Powershell script that interrogates the Octopus API and then sends certain information to the DataDog API.

I’ve written a “Datadog - Log Task” step template that can be added to all projects you want to send logs to Datadog.

To add this step template to your deployment or runbook process, you can either add a step and search for “Datadog” and add it from the community library, or go to the “Library” menu, then “Step Templates”, and then “Browse Library”.

The script in the step template calls the API to get the task logs for the current task id and then takes the response and shapes it to the format required to send the Datadog API.

function Send-DatadogEvent (
    $datadog,
    [string] $text,
    [string] $level,
    $properties = @{},
    [string] $exception = $null,
    [switch] $template) {
    
    
    if (-not $level) {
        $level = 'Information'
    }

    if (@('Verbose', 'Debug', 'Information', 'Warning', 'Error', 'Fatal') -notcontains $level) {
        $level = 'Information'
    }


    $ddtags = "project:$($properties.ProjectName),deploymentname:$($properties.DeploymentName),env:$($properties.EnvironmentName)"
    if ($properties["TaskType"] -eq "Runbook") {
        $ddtags += ",runbookname:$($properties.RunbookName),tasktype:runbook"
    }
    else {
        $ddtags += ",tasktype:deployment"    
    }

    $body = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
    $body.Add("ddsource", "Octopus Deploy")
    $body.Add("ddtags", $ddtags)
    $body.Add("service", $DatadogServiceName)
    $body.Add("hostname", "https://octopus.the-crock.com/")
    $body.Add("http.url", "$($properties["TaskLink"])")
    $body.Add("octopus.deployment.properties", "$($properties | ConvertTo-Json)")

    if ($exception) {
        $body.Add("error.message", "$($properties["Error"])")
        $body.Add("error.stack", "$($exception)")
    }
    
    $body.Add("level", "$($level)")
  
    $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
    $headers.Add("Content-Type", "application/json")
    $headers.Add("DD-APPLICATION-KEY", "$($DatadogApplicationKey)")
    $headers.Add("DD-API-KEY", "$($DatadogApiKey)")

    Invoke-RestMethod -Uri $DatadogUrl -Body $($body | ConvertTo-Json)  -ContentType "application/json" -Method POST -Headers $headers
}

function Set-ErrorDetails(){

    $octopusAPIHeader = @{ "X-Octopus-ApiKey" = $DatadogOctopusAPIKey }
    $taskDetailUri = "$($OctopusParameters['Octopus.Web.ServerUri'])/api/tasks/$($OctopusParameters["Octopus.Task.Id"])/details"

    $taskDetails = Invoke-RestMethod -Method Get -Uri $taskDetailUri -Headers $octopusAPIHeader 
    $errorMessage = "";
    $errorFirstLine = "";
    $isFirstLine = $true;

    foreach ($activityLog in $taskDetails.ActivityLogs) {
        foreach ($activityLogChild1 in $activityLog.Children) {
            foreach ($activityLogChild2 in $activityLogChild1.Children) {
                foreach ($logElement in $activityLogChild2.LogElements) {
                    if ($logElement.Category -eq "Error") {
                        if ($isFirstLine -eq $true) {
                            $errorFirstLine = $logElement.MessageText;
                            $isFirstLine = $false;
                        }

                        $errorMessage += $logElement.MessageText + " `n"
                    }
                }
            }
        }
    }

    $exInfo = @{
        firstLine = $errorFirstLine
        message = $errorMessage
    }

    return $exInfo;
}

function Set-TaskProperties(){
    $taskProperties = @{
        ProjectName     = $OctopusParameters['Octopus.Project.Name'];
        Result          = "succeeded";
        InstanceUrl     = $OctopusParameters['Octopus.Web.ServerUri'];
        EnvironmentName = $OctopusParameters['Octopus.Environment.Name'];
        DeploymentName  = $OctopusParameters['Octopus.Deployment.Name'];
        TenantName      = $OctopusParameters["Octopus.Deployment.Tenant.Name"]
        TaskLink        = $taskLink
    }
    
    if ([string]::IsNullOrEmpty($OctopusParameters["Octopus.Runbook.Id"]) -eq $false) {
        $taskProperties["TaskType"] 			= "Runbook"
        $taskProperties["RunbookSnapshotName"] 	= $OctopusParameters["Octopus.RunbookSnapshot.Name"]
        $taskProperties["RunbookName"]         	= $OctopusParameters["Octopus.Runbook.Name"]
    }
    else {
        $taskProperties["TaskType"] 		= "Deployment"
        $taskProperties["ReleaseNumber"] 	= $OctopusParameters['Octopus.Release.Number'];
        $taskProperties["Channel"]  		= $OctopusParameters['Octopus.Release.Channel.Name'];
    }

    return $taskProperties;
}

#******************************************************************

$taskLink = $OctopusParameters['Octopus.Web.ServerUri'] + "/app#/" + $OctopusParameters["Octopus.Space.Id"] + "/tasks/" + $OctopusParameters["Octopus.Task.Id"]
$level = "Information"
$exception = $null

Write-Output "Logging the deployment result to Datadog at $DatadogServerUrl..."

$properties = Set-TaskProperties

if ($OctopusParameters['Octopus.Deployment.Error']) {
    $exceptionInfo = Set-ErrorDetails
    $properties["Result"] = "failed"
    $properties["Error"] = $exceptionInfo["firstLine"]
    $exception = $exceptionInfo["message"]
    $level = "Error"
}

try {
    Send-DatadogEvent $datadog "A deployment of $($properties.ProjectName) release $($properties.ReleaseNumber) $($properties.Result) in $($properties.EnvironmentName)" -level $level -template -properties $properties -exception $exception
}
catch [Exception] {
    Write-Error "Unable to write task details to Datadog"
    $_.Exception | format-list -force
}

This image shows the results in Datadog for a successful task, with just information level data:

This image shows an error log:

And this image shows an error from a runbook process:

Using run conditions you can specify that you only want a task log to be sent when the deployment fails, succeeds, or, if you want error and successful runs to be logged, set it to “Always run”.

If you’d like to send your task logs to another external log application, take a look at the Powershell script in the Datadog step template to help you get started. If you create a step that you think others will find useful, please submit it to our Community Step Template library!

2 Likes

Just wanted to give you a shout out. This is very good and very useful.
After deploying your step template to my server and a few configs it just worked and worked really well.

I did have to make some changes to remove the static URL you have in code :wink:
I also wanted to pull in extra data. With the help of your template i was able to do this with ease.

Thank you for creating this.

1 Like

That’s great to hear, thank you for letting me know!