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