Skip to content

Commit 8968da9

Browse files
authored
Linux Consumption metrics publisher for Legion (#10942)
1 parent 4b00fcb commit 8968da9

24 files changed

+1045
-284
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Configuration
5+
{
6+
public sealed class LinuxConsumptionLegionMetricsPublisherOptions
7+
{
8+
internal const int DefaultMetricsPublishIntervalMS = 30 * 1000;
9+
10+
public int MetricsPublishIntervalMS { get; set; } = DefaultMetricsPublishIntervalMS;
11+
12+
public string ContainerName { get; set; }
13+
14+
public string MetricsFilePath { get; set; }
15+
}
16+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.Options;
5+
6+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Configuration
7+
{
8+
public class LinuxConsumptionLegionMetricsPublisherOptionsSetup : IConfigureOptions<LinuxConsumptionLegionMetricsPublisherOptions>
9+
{
10+
private readonly IEnvironment _environment;
11+
12+
public LinuxConsumptionLegionMetricsPublisherOptionsSetup(IEnvironment environment)
13+
{
14+
_environment = environment;
15+
}
16+
17+
public void Configure(LinuxConsumptionLegionMetricsPublisherOptions options)
18+
{
19+
options.ContainerName = _environment.GetEnvironmentVariable(EnvironmentSettingNames.ContainerName);
20+
options.MetricsFilePath = _environment.GetEnvironmentVariable(EnvironmentSettingNames.FunctionsMetricsPublishPath);
21+
}
22+
}
23+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.IO.Abstractions;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
10+
using Microsoft.Extensions.Logging;
11+
using Newtonsoft.Json;
12+
13+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Metrics
14+
{
15+
internal sealed class LegionMetricsFileManager
16+
{
17+
private readonly IFileSystem _fileSystem;
18+
private readonly int _maxFileCount;
19+
private readonly ILogger _logger;
20+
private readonly JsonSerializerSettings _serializerSettings;
21+
22+
public LegionMetricsFileManager(string metricsFilePath, IFileSystem fileSystem, ILogger logger, int maxFileCount)
23+
{
24+
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
25+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
26+
MetricsFilePath = metricsFilePath;
27+
_maxFileCount = maxFileCount;
28+
29+
_serializerSettings = new JsonSerializerSettings
30+
{
31+
NullValueHandling = NullValueHandling.Ignore
32+
};
33+
}
34+
35+
// Internal access for testing only
36+
internal string MetricsFilePath { get; set; }
37+
38+
private bool PrepareDirectoryForFile()
39+
{
40+
if (string.IsNullOrEmpty(MetricsFilePath))
41+
{
42+
return false;
43+
}
44+
45+
// ensure the directory exists
46+
var metricsDirectoryInfo = _fileSystem.Directory.CreateDirectory(MetricsFilePath);
47+
48+
// ensure we're under the max file count
49+
var files = metricsDirectoryInfo.GetFiles().OrderBy(p => p.CreationTime).ToList();
50+
if (files.Count < _maxFileCount)
51+
{
52+
return true;
53+
}
54+
55+
// we're at or over limit
56+
// delete enough files that we have space to write a new one
57+
int numToDelete = files.Count - _maxFileCount + 1;
58+
var filesToDelete = files.Take(numToDelete).ToArray();
59+
60+
_logger.LogDebug($"Deleting {filesToDelete.Length} metrics file(s).");
61+
62+
Parallel.ForEach(filesToDelete, file =>
63+
{
64+
try
65+
{
66+
file.Delete();
67+
}
68+
catch (Exception ex) when (!ex.IsFatal())
69+
{
70+
// best effort
71+
_logger.LogError(ex, $"Error deleting metrics file '{file.FullName}'.");
72+
}
73+
});
74+
75+
files = metricsDirectoryInfo.GetFiles().OrderBy(p => p.CreationTime).ToList();
76+
77+
// return true if we have space for a new file
78+
return files.Count < _maxFileCount;
79+
}
80+
81+
public async Task PublishMetricsAsync(object metrics)
82+
{
83+
string fileName = string.Empty;
84+
85+
try
86+
{
87+
bool metricsPublishEnabled = !string.IsNullOrEmpty(MetricsFilePath);
88+
if (metricsPublishEnabled && !PrepareDirectoryForFile())
89+
{
90+
return;
91+
}
92+
93+
string metricsContent = JsonConvert.SerializeObject(metrics, _serializerSettings);
94+
_logger.PublishingMetrics(metricsContent);
95+
96+
if (metricsPublishEnabled)
97+
{
98+
fileName = $"{Guid.NewGuid().ToString().ToLowerInvariant()}.json";
99+
string filePath = Path.Combine(MetricsFilePath, fileName);
100+
101+
using var streamWriter = _fileSystem.File.CreateText(filePath);
102+
await streamWriter.WriteAsync(metricsContent);
103+
}
104+
}
105+
catch (Exception ex) when (!ex.IsFatal())
106+
{
107+
// TODO: consider using a retry strategy here
108+
_logger.LogError(ex, $"Error writing metrics file '{fileName}'.");
109+
}
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)