Skip to content

Commit

Permalink
feat: Azure DevOps logging commands (#43)
Browse files Browse the repository at this point in the history
+semver: feature
  • Loading branch information
ewingjm authored Mar 3, 2021
1 parent de8a623 commit ed6a118
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 16 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This project's aim is to build a powerful base Package Deployer template that si
- [Import word templates](#Import-word-templates)
- [Mailboxes](#Mailboxes)
- [Update, approve, test and enable shared mailboxes](#Update-approve-test-and-enable-shared-mailboxes)
- [Azure Pipelines](#Azure-pipelines)
- [Contributing](#Contributing)
- [Licence](#Licence)

Expand Down Expand Up @@ -195,6 +196,12 @@ Import-CrmPackage –CrmConnection $conn –PackageDirectory $packageDir –Pack
The runtime setting takes precedence if both an environment variable and runtime setting are found for the same shared mailbox.


## Azure Pipelines

The template will automatically detect if your deployment is running on Azure Pipelines and will log with the appropriate [logging commands](https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands) to ensure that warnings and errors are reported on your pipeline or release.

Use the `TraceLoggerAdapter` rather than the `PackageLog` to ensure that your extensions also integrate seamlessly with Azure Pipelines.

## Contributing

Please refer to the [Contributing](./CONTRIBUTING.md) guide.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Capgemini.PowerApps.PackageDeployerTemplate.Adapters
{
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Tooling.PackageDeployment.CrmPackageExtentionBase;

/// <summary>
/// An adapter class from <see cref="TraceLogger"/> to <see cref="ILogger"/> for use on Azure DevOps.
/// </summary>
[ExcludeFromCodeCoverage]
public class AzureDevOpsTraceLoggerAdapter : TraceLoggerAdapter
{
/// <summary>
/// Initializes a new instance of the <see cref="AzureDevOpsTraceLoggerAdapter"/> class.
/// </summary>
/// <param name="traceLogger">The <see cref="TraceLogger"/>.</param>
public AzureDevOpsTraceLoggerAdapter(TraceLogger traceLogger)
: base(traceLogger)
{
}

/// <inheritdoc/>
protected override string GetPrefix(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Warning:
return "##[task.logissue type=warning]";
case LogLevel.Error:
case LogLevel.Critical:
return "##[task.logissue type=error]";
default:
return string.Empty;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Tooling.PackageDeployment.CrmPackageExtentionBase;

/// <summary>
/// An adapter class from <see cref="TraceLogger"/> to <see cref="ILogger"/>.
/// </summary>
internal class TraceLoggerAdapter : ILogger
[ExcludeFromCodeCoverage]
public class TraceLoggerAdapter : ILogger
{
private static readonly Dictionary<LogLevel, TraceEventType> LogLevelMap = new Dictionary<LogLevel, TraceEventType>
{
Expand All @@ -29,9 +31,24 @@ internal class TraceLoggerAdapter : ILogger
/// <param name="traceLogger">The <see cref="TraceLogger"/>.</param>
public TraceLoggerAdapter(TraceLogger traceLogger)
{
this.traceLogger = traceLogger ?? throw new ArgumentNullException(nameof(traceLogger));
if (traceLogger is null)
{
throw new ArgumentNullException(nameof(traceLogger));
}

(this.traceLogger, this.Warnings, this.Errors) = (traceLogger, new List<string>(), new List<string>());
}

/// <summary>
/// Gets warning messages logged.
/// </summary>
public IList<string> Warnings { get; }

/// <summary>
/// Gets error messages logged.
/// </summary>
public IList<string> Errors { get; }

/// <inheritdoc/>
public IDisposable BeginScope<TState>(TState state) => default;

Expand All @@ -46,10 +63,29 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
return;
}

var logMessage = string.Format("{0} {1}", formatter(state, exception), exception != null ? exception.StackTrace : string.Empty);
var traceEventType = LogLevelMap[logLevel];
var message = formatter(state, exception);
if (logLevel == LogLevel.Warning)
{
this.Warnings.Add(message);
}
else if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical)
{
this.Errors.Add(message);
}

this.traceLogger.Log(logMessage, traceEventType);
this.traceLogger.Log(
$"{this.GetPrefix(logLevel)}{message} {(exception != null ? exception.StackTrace : string.Empty)}",
LogLevelMap[logLevel]);
}

/// <summary>
/// Gets the prefix for a given log level.
/// </summary>
/// <param name="logLevel">The log level.</param>
/// <returns>The prefix.</returns>
protected virtual string GetPrefix(LogLevel logLevel)
{
return string.Empty;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using Capgemini.PowerApps.PackageDeployerTemplate.Adapters;
using Capgemini.PowerApps.PackageDeployerTemplate.Config;
using Capgemini.PowerApps.PackageDeployerTemplate.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Tooling.Connector;
using Microsoft.Xrm.Tooling.PackageDeployment.CrmPackageExtentionBase;

Expand All @@ -30,6 +31,11 @@ public abstract class PackageTemplateBase : ImportExtension
private ConnectionReferenceDeploymentService connectionReferenceSvc;
private MailboxDeploymentService mailboxSvc;

/// <summary>
/// Gets a value indicating whether whether the deployment is running on Azure DevOps.
/// </summary>
protected static bool RunningOnAzureDevOps => bool.TryParse(Environment.GetEnvironmentVariable("TF_BUILD"), out var tfBuild) && tfBuild;

/// <summary>
/// Gets the path to the package folder.
/// </summary>
Expand Down Expand Up @@ -94,7 +100,7 @@ protected ICrmServiceAdapter LicensedCrmServiceAdapter
}
else
{
this.PackageLog.Log("No licensed user credentials found.", TraceEventType.Information);
this.TraceLoggerAdapter.LogInformation("No licensed user credentials found.");
}
}

Expand Down Expand Up @@ -246,13 +252,16 @@ protected TemplateConfig TemplateConfig
}
}

private TraceLoggerAdapter TraceLoggerAdapter
/// <summary>
/// Gets a <see cref="TraceLogger"/> adapter that provides additional functionality (e.g. for Azure Pipelines).
/// </summary>
protected TraceLoggerAdapter TraceLoggerAdapter
{
get
{
if (this.traceLoggerAdapter == null)
{
this.traceLoggerAdapter = new TraceLoggerAdapter(this.PackageLog);
this.traceLoggerAdapter = RunningOnAzureDevOps ? new AzureDevOpsTraceLoggerAdapter(this.PackageLog) : new TraceLoggerAdapter(this.PackageLog);
}

return this.traceLoggerAdapter;
Expand Down Expand Up @@ -348,6 +357,11 @@ public override bool AfterPrimaryImport()
this.PackageFolderPath);
this.MailboxSvc.UpdateApproveAndEnableMailboxes(this.MailboxMappings);
if (RunningOnAzureDevOps)
{
this.LogTaskCompleteResult();
}
});

return true;
Expand Down Expand Up @@ -403,7 +417,7 @@ protected T GetSetting<T>(string key)
/// <returns>The setting value (if found).</returns>
protected IDictionary<string, string> GetSettings(string prefix)
{
this.PackageLog.Log($"Getting {prefix} settings", TraceEventType.Verbose);
this.TraceLoggerAdapter.LogDebug($"Getting {prefix} settings");

var environmentVariables = Environment.GetEnvironmentVariables();
var mappings = environmentVariables.Keys
Expand All @@ -413,7 +427,7 @@ protected IDictionary<string, string> GetSettings(string prefix)
k => k.Remove(0, Constants.Settings.EnvironmentVariablePrefix.Length + prefix.Length + 1).ToLower(),
v => environmentVariables[v].ToString());

this.PackageLog.Log($"{mappings.Count} matching settings found in environment variables", TraceEventType.Verbose);
this.TraceLoggerAdapter.LogDebug($"{mappings.Count} matching settings found in environment variables");

if (this.RuntimeSettings == null)
{
Expand All @@ -424,13 +438,13 @@ protected IDictionary<string, string> GetSettings(string prefix)
.Where(s => s.Key.StartsWith($"{prefix}:"))
.ToDictionary(s => s.Key.Remove(0, prefix.Length + 1), s => s.Value.ToString());

this.PackageLog.Log($"{mappings.Count} matching settings found in runtime settings", TraceEventType.Verbose);
this.TraceLoggerAdapter.LogDebug($"{mappings.Count} matching settings found in runtime settings");

foreach (var runtimeSettingsMapping in runtimeSettingMappings)
{
if (mappings.ContainsKey(runtimeSettingsMapping.Key))
{
this.PackageLog.Log($"Overriding environment variable setting with runtime setting for {runtimeSettingsMapping.Key}.");
this.TraceLoggerAdapter.LogInformation($"Overriding environment variable setting with runtime setting for {runtimeSettingsMapping.Key}.");
}

mappings[runtimeSettingsMapping.Key] = runtimeSettingsMapping.Value;
Expand All @@ -448,12 +462,26 @@ private void ExecuteLifecycleEvent(string eventName, Action eventAction)

private void LogLifecycleEventStart(string lifecycleEvent)
{
this.PackageLog.Log($"{nameof(PackageTemplateBase)}.{lifecycleEvent} running...", TraceEventType.Information);
this.TraceLoggerAdapter.LogInformation($"{nameof(PackageTemplateBase)}.{lifecycleEvent} running...");
}

private void LogLifecycleEventEnd(string lifecycleEvent)
{
this.PackageLog.Log($"{nameof(PackageTemplateBase)}.{lifecycleEvent} completed.", TraceEventType.Information);
this.TraceLoggerAdapter.LogInformation($"{nameof(PackageTemplateBase)}.{lifecycleEvent} completed.");
}

// Excluded as it would require our CI or PR validation pipelines to be partially succeeding or failing
[ExcludeFromCodeCoverage]
private void LogTaskCompleteResult()
{
if (this.TraceLoggerAdapter.Errors.Any())
{
Console.WriteLine("##vso[task.complete result=Failed;]DONE");
}
else if (this.TraceLoggerAdapter.Warnings.Any())
{
Console.WriteLine("##vso[task.complete result=SucceededWithIssues;]DONE");
}
}
}
}

0 comments on commit ed6a118

Please sign in to comment.