Skip to content

Commit 5035150

Browse files
authored
Refactor logic to enable providing experiment ID and additional metadata to summary file logger. (#525)
1 parent 7a74cf4 commit 5035150

11 files changed

Lines changed: 207 additions & 37 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.0.22
1+
2.0.23

src/VirtualClient/VirtualClient.Common/Extensions/StringExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace VirtualClient.Common.Extensions
66
using System;
77
using System.Collections;
88
using System.Diagnostics.CodeAnalysis;
9+
using System.Linq;
910
using System.Runtime.InteropServices;
1011
using System.Security;
1112
using System.Text.RegularExpressions;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Moq;
7+
using NUnit.Framework;
8+
9+
namespace VirtualClient.Contracts
10+
{
11+
[TestFixture]
12+
[Category("Unit")]
13+
internal class FileContextTests
14+
{
15+
private MockFixture mockFixture;
16+
17+
public void SetupDefaults()
18+
{
19+
this.mockFixture = new MockFixture();
20+
this.mockFixture.Setup(PlatformID.Unix);
21+
}
22+
23+
[Test]
24+
[TestCase("{experimentId}-{agentId}-summary.txt", "ab43a99d-eddd-44f8-ac25-f368f02dbc21-agent01-summary.txt")]
25+
[TestCase("{experimentId}-{toolName}-summary.txt", "ab43a99d-eddd-44f8-ac25-f368f02dbc21-tool01-summary.txt")]
26+
public void FileUploadDescriptorFactoryCreatesTheExpectedDescriptor_When_Timestamped(string pathTemplate, string expectedResolvedPath)
27+
{
28+
this.SetupDefaults();
29+
30+
IDictionary<string, IConvertible> replacements = new Dictionary<string, IConvertible>
31+
{
32+
["experimentId"] = "ab43a99d-eddd-44f8-ac25-f368f02dbc21",
33+
["agentId"] = "agent01",
34+
["toolName"] = "tool01"
35+
};
36+
37+
string actualResolvedPath = FileContext.ResolvePathTemplate(pathTemplate, replacements);
38+
Assert.AreEqual(expectedResolvedPath, actualResolvedPath);
39+
}
40+
}
41+
}

src/VirtualClient/VirtualClient.Contracts/FileContext.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
namespace VirtualClient.Contracts
55
{
6+
using System;
7+
using System.Collections.Generic;
68
using System.IO.Abstractions;
9+
using System.Linq;
10+
using System.Text.RegularExpressions;
711
using VirtualClient.Common.Extensions;
812

913
/// <summary>
@@ -12,6 +16,8 @@ namespace VirtualClient.Contracts
1216
/// </summary>
1317
public class FileContext
1418
{
19+
private static readonly Regex TemplatePlaceholderExpression = new Regex(@"\{(.*?)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
20+
1521
/// <summary>
1622
/// Initializes a new instance of the <see cref="FileContext"/> class.
1723
/// </summary>
@@ -87,5 +93,71 @@ public FileContext(IFileInfo file, string contentType, string contentEncoding, s
8793
/// The name of the tool/toolset that produced the file (e.g. FioExecutor, FIO).
8894
/// </summary>
8995
public string ToolName { get; }
96+
97+
/// <summary>
98+
/// Resolves placeholders in the path template provided.
99+
/// </summary>
100+
/// <param name="pathTemplate">A path template containing placeholders to resolve (e.g. {experimentId}-summary.txt).</param>
101+
/// <param name="replacements">Provides the replacement values for the placeholders in the path template.</param>
102+
/// <returns>
103+
/// A path having matching placeholders replaced with actual values
104+
/// (e.g. {experimentId}-summary.txt -> afda108a-4be9-4fe2-a9ef-7b787150896a-summary.txt).
105+
/// </returns>
106+
public static string ResolvePathTemplate(string pathTemplate, IDictionary<string, IConvertible> replacements)
107+
{
108+
string resolvedTemplate = pathTemplate;
109+
MatchCollection matches = FileContext.TemplatePlaceholderExpression.Matches(pathTemplate);
110+
111+
if (matches?.Any() == true)
112+
{
113+
string resolvedValue;
114+
foreach (Match match in matches)
115+
{
116+
string[] effectivePlaceholders = null;
117+
string templatePlaceholder = match.Groups[1].Value;
118+
if (templatePlaceholder.IndexOf('|') < 0)
119+
{
120+
effectivePlaceholders = new string[] { templatePlaceholder };
121+
}
122+
else
123+
{
124+
effectivePlaceholders = templatePlaceholder.Split("|", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
125+
}
126+
127+
bool placeholderMatched = false;
128+
foreach (string placeholder in effectivePlaceholders)
129+
{
130+
// Order of placeholder resolution:
131+
// 1) Metadata known by the VC runtime is applied first because it is definitive.
132+
// 2) Component metadata supplied to the factory.
133+
// 3) Component parameters supplied to the factory.
134+
if (replacements?.Any() == true && FileContext.TryResolvePlaceholder(replacements, placeholder, out resolvedValue))
135+
{
136+
placeholderMatched = true;
137+
resolvedTemplate = resolvedTemplate.Replace(match.Value, resolvedValue);
138+
break;
139+
}
140+
}
141+
142+
if (!placeholderMatched)
143+
{
144+
resolvedTemplate = resolvedTemplate.Replace(match.Value, string.Empty);
145+
}
146+
}
147+
}
148+
149+
return resolvedTemplate;
150+
}
151+
152+
private static bool TryResolvePlaceholder(IDictionary<string, IConvertible> metadata, string propertyName, out string resolvedValue)
153+
{
154+
resolvedValue = null;
155+
if (!string.IsNullOrWhiteSpace(propertyName) && metadata.TryGetValue(propertyName, out IConvertible propertyValue) && propertyValue != null)
156+
{
157+
resolvedValue = propertyValue.ToString();
158+
}
159+
160+
return resolvedValue != null;
161+
}
90162
}
91163
}

src/VirtualClient/VirtualClient.Contracts/VirtualClientComponent.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public abstract class VirtualClientComponent : IDisposable
3030
/// </summary>
3131
public static readonly char[] CommonDelimiters = new char[] { ',', ';' };
3232

33+
/// <summary>
34+
/// Common delimiters for parameter set collections. The delimiters are defined in
35+
/// priority order for parsing operations.
36+
/// </summary>
37+
public static readonly string[] CommonParameterDelimiters = new string[] { ",,,", ";", "," };
38+
3339
/// <summary>
3440
/// The assembly containing the component base class and types.
3541
/// </summary>

src/VirtualClient/VirtualClient.Contracts/VirtualClientRuntime.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
namespace VirtualClient
55
{
66
using System;
7-
using System.Collections.Concurrent;
87
using System.Collections.Generic;
8+
using System.Diagnostics;
99
using System.Linq;
1010
using System.Threading;
1111
using System.Threading.Tasks;
@@ -52,6 +52,26 @@ public static class VirtualClientRuntime
5252
/// </summary>
5353
public static List<Action_> CleanupTasks { get; } = new List<Action_>();
5454

55+
/// <summary>
56+
/// The command line arguments provided to the Virtual Client application.
57+
/// </summary>
58+
public static string[] CommandLineArguments { get; internal set; }
59+
60+
/// <summary>
61+
/// The current experiment ID for the application.
62+
/// </summary>
63+
public static string ExperimentId { get; internal set; }
64+
65+
/// <summary>
66+
/// The current platform-specifics for the application.
67+
/// </summary>
68+
public static PlatformSpecifics PlatformSpecifics { get; internal set; }
69+
70+
/// <summary>
71+
/// The name of the Virtual Client application/module.
72+
/// </summary>
73+
public static string ExecutableName { get; internal set; } = Process.GetCurrentProcess().MainModule.FileName;
74+
5575
/// <summary>
5676
/// A set of one or more tasks (exit) registered to execute before the application
5777
/// exits completely. The dictionary key can be used to determine if a particular task exists

src/VirtualClient/VirtualClient.Core/Logging/SummaryFile/SummaryFileLogger.cs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,19 @@ namespace VirtualClient.Logging
55
{
66
using System;
77
using System.Collections.Generic;
8-
using System.ComponentModel;
98
using System.Data;
109
using System.IO;
1110
using System.IO.Abstractions;
1211
using System.Linq;
13-
using System.Runtime.InteropServices;
1412
using System.Text;
15-
using System.Text.RegularExpressions;
1613
using System.Threading;
1714
using System.Threading.Tasks;
18-
using Microsoft.CodeAnalysis.CSharp.Syntax;
1915
using Microsoft.Extensions.Logging;
2016
using Newtonsoft.Json;
2117
using Newtonsoft.Json.Linq;
2218
using Polly;
2319
using VirtualClient.Common;
20+
using VirtualClient.Common.Extensions;
2421
using VirtualClient.Common.Telemetry;
2522
using VirtualClient.Contracts;
2623
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -51,18 +48,16 @@ public class SummaryFileLogger : ILogger, IFlushableChannel, IDisposable
5148
/// <summary>
5249
/// Initializes a new instance of the <see cref="SummaryFileLogger"/> class.
5350
/// </summary>
54-
/// <param name="filePath">The path to the CSV file to which the metrics should be written.</param>
55-
/// <param name="retryPolicy"></param>
51+
/// <param name="filePath">The path where the summary file should be written.</param>
52+
/// <param name="retryPolicy">A retry policy to apply to transient errored attempts to access the summary file for write operations.</param>
5653
public SummaryFileLogger(string filePath, IAsyncPolicy retryPolicy = null)
5754
{
58-
if (string.IsNullOrWhiteSpace(filePath))
59-
{
60-
PlatformSpecifics tempPlatformSpecifics = new PlatformSpecifics(Environment.OSVersion.Platform, RuntimeInformation.ProcessArchitecture);
61-
filePath = tempPlatformSpecifics.Combine(tempPlatformSpecifics.LogsDirectory, DefaultFileName);
62-
}
55+
filePath.ThrowIfNullOrWhiteSpace(nameof(filePath));
6356

64-
this.filePath = filePath;
65-
this.fileDirectory = Path.GetDirectoryName(filePath);
57+
string effectiveFilePath = Path.GetFullPath(filePath);
58+
59+
this.filePath = effectiveFilePath;
60+
this.fileDirectory = Path.GetDirectoryName(effectiveFilePath);
6661
this.fileSystem = new FileSystem();
6762
this.buffer = new ConcurrentBuffer();
6863
this.fileAccessRetryPolicy = retryPolicy ?? Policy.Handle<IOException>().WaitAndRetryAsync(5, (retries) => TimeSpan.FromSeconds(retries));
@@ -282,7 +277,7 @@ private static string CreateProfileBeginMessage(EventContext context)
282277
messageBuilder.AppendMessage($"Profile: {context.Properties["executionProfile"].ToString()}");
283278
messageBuilder.AppendMessage($"Execution Arguments: {context.Properties["executionArguments"].ToString()}");
284279
messageBuilder.AppendMessage($"Experiment Id: {context.Properties["experimentId"].ToString()}");
285-
280+
286281
var dependencies = context.Properties["executionProfileDependencies"] as JArray;
287282

288283
if (dependencies != null)

src/VirtualClient/VirtualClient.Core/Logging/SummaryFile/SummaryFileLoggerProvider.cs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
namespace VirtualClient.Logging
55
{
66
using System;
7+
using System.Runtime.InteropServices;
78
using Microsoft.Extensions.Logging;
8-
using VirtualClient.Common.Extensions;
99
using VirtualClient.Contracts;
1010
using ILogger = Microsoft.Extensions.Logging.ILogger;
1111

1212
/// <summary>
1313
/// Provides methods for creating <see cref="ILogger"/> instances that can be used
14-
/// to write summary file.
14+
/// to write summary log files.
1515
/// </summary>
1616
public sealed class SummaryFileLoggerProvider : ILoggerProvider
1717
{
@@ -27,17 +27,32 @@ public SummaryFileLoggerProvider(string filePath)
2727
}
2828

2929
/// <summary>
30-
/// Creates an <see cref="ILogger"/> instance that can be used to log events/messages
31-
/// to an Application Insights endpoint.
30+
/// Creates an <see cref="ILogger"/> instance that can be used write events to a
31+
/// summary log.
3232
/// </summary>
3333
/// <param name="categoryName">The logger events category.</param>
34-
/// <returns>
35-
/// An <see cref="ILogger"/> instance that can log events/messages to an Application
36-
/// Insights endpoint.
37-
/// </returns>
3834
public ILogger CreateLogger(string categoryName)
3935
{
40-
SummaryFileLogger logger = new SummaryFileLogger(this.filePath);
36+
string effectiveFilePath = this.filePath;
37+
38+
if (string.IsNullOrWhiteSpace(effectiveFilePath))
39+
{
40+
PlatformSpecifics platformSpecifics = VirtualClientRuntime.PlatformSpecifics
41+
?? new PlatformSpecifics(Environment.OSVersion.Platform, RuntimeInformation.ProcessArchitecture);
42+
43+
string experimentId = VirtualClientRuntime.ExperimentId;
44+
string logsPath = platformSpecifics.GetLogsPath();
45+
string summaryFileName = "summary.txt";
46+
47+
if (!string.IsNullOrWhiteSpace(experimentId))
48+
{
49+
summaryFileName = $"{experimentId}-summary.txt";
50+
}
51+
52+
effectiveFilePath = platformSpecifics.Combine(logsPath, summaryFileName);
53+
}
54+
55+
SummaryFileLogger logger = new SummaryFileLogger(effectiveFilePath);
4156
VirtualClientRuntime.CleanupTasks.Add(new Action_(() =>
4257
{
4358
logger.Flush();

src/VirtualClient/VirtualClient.Main/CommandBase.cs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -339,27 +339,46 @@ protected IList<ILoggerProvider> CreateLoggerProviders(IConfiguration configurat
339339
// backward compatibility for --eventhub
340340
if (!string.IsNullOrEmpty(this.EventHubStore))
341341
{
342-
this.loggerDefinitions.Add($"eventhub={this.EventHubStore}");
342+
this.loggerDefinitions.Add($"eventhub;{this.EventHubStore}");
343343
}
344344

345345
if (!this.loggerDefinitions.Any(l => l.Equals("proxy", StringComparison.OrdinalIgnoreCase) || l.StartsWith("proxy=", StringComparison.OrdinalIgnoreCase))
346346
&& this.ProxyApiUri != null)
347347
{
348-
this.loggerDefinitions.Add($"proxy={this.ProxyApiUri.ToString()}");
348+
this.loggerDefinitions.Add($"proxy;{this.ProxyApiUri.ToString()}");
349349
}
350350

351351
LogLevel loggingLevel = this.LoggingLevel ?? LogLevel.Information;
352352

353353
foreach (string loggerDefinition in this.loggerDefinitions)
354354
{
355355
string loggerName = loggerDefinition;
356-
string definitionValue = string.Empty;
357-
if (loggerDefinition.Contains("="))
356+
string loggerParameters = string.Empty;
357+
358+
// e.g.
359+
// --logger=SummaryFileLoggerProvider;../logs/{experimentId}-summary.txt
360+
int indexOfDelimiter = loggerDefinition.IndexOf(';');
361+
if (indexOfDelimiter >= 0)
358362
{
359-
loggerName = loggerDefinition.Substring(0, loggerDefinition.IndexOf("=", StringComparison.Ordinal)).Trim();
360-
definitionValue = loggerDefinition.Substring(loggerDefinition.IndexOf("=", StringComparison.Ordinal) + 1);
363+
loggerName = loggerName.Substring(0, indexOfDelimiter);
364+
loggerParameters = loggerDefinition.Substring(indexOfDelimiter + 1);
361365
}
362366

367+
// Support placeholder replacements (e.g. {experimentId}, {agentId}).
368+
IDictionary<string, IConvertible> replacements = new Dictionary<string, IConvertible>(StringComparer.OrdinalIgnoreCase)
369+
{
370+
{ "experimentId", this.ExperimentId },
371+
{ "agentId", this.ClientId },
372+
{ "clientId", this.ClientId }
373+
};
374+
375+
if (this.Metadata?.Any() == true)
376+
{
377+
replacements.AddRange(this.Metadata);
378+
}
379+
380+
loggerParameters = FileContext.ResolvePathTemplate(loggerParameters, replacements);
381+
363382
switch (loggerName.ToLowerInvariant())
364383
{
365384
case "console":
@@ -371,12 +390,12 @@ protected IList<ILoggerProvider> CreateLoggerProviders(IConfiguration configurat
371390
break;
372391

373392
case "eventhub":
374-
DependencyEventHubStore store = EndpointUtility.CreateEventHubStoreReference(DependencyStore.Telemetry, endpoint: definitionValue, this.CertificateManager ?? new CertificateManager());
393+
DependencyEventHubStore store = EndpointUtility.CreateEventHubStoreReference(DependencyStore.Telemetry, endpoint: loggerParameters, this.CertificateManager ?? new CertificateManager());
375394
CommandBase.AddEventHubLogging(loggingProviders, configuration, store, loggingLevel);
376395
break;
377396

378397
case "proxy":
379-
CommandBase.AddProxyApiLogging(loggingProviders, configuration, platformSpecifics, new Uri(definitionValue), source);
398+
CommandBase.AddProxyApiLogging(loggingProviders, configuration, platformSpecifics, new Uri(loggerParameters), source);
380399
break;
381400

382401
default:
@@ -387,7 +406,7 @@ protected IList<ILoggerProvider> CreateLoggerProviders(IConfiguration configurat
387406
$"or is not defined in the extensions assemblies provided to the application.");
388407
}
389408

390-
ILoggerProvider customLoggerProvider = (ILoggerProvider)Activator.CreateInstance(subcomponentType, definitionValue);
409+
ILoggerProvider customLoggerProvider = (ILoggerProvider)Activator.CreateInstance(subcomponentType, loggerParameters);
391410
loggingProviders.Add(customLoggerProvider);
392411
break;
393412
}

0 commit comments

Comments
 (0)