Skip to content

Commit 9a65ba4

Browse files
authored
Additional logging limitations and control (#5888)
## Change Expands the limitations on log files and allows them to be user configurable. The existing limit was hardcoded to remove any files over 7 days old. This was enforced by a background thread spun up during process start. This same thread enforces the new limits on the aggregate logging directory, while additional code in the file logger enforces the individual file size limitation. When new log lines would exceed the maximum file size for an individual file, the stream is repositioned after the initialization portion of the log, a log line is output to indicate the wrap, and the logs continue to overwrite the older data.
1 parent 08b4a6a commit 9a65ba4

File tree

20 files changed

+782
-27
lines changed

20 files changed

+782
-27
lines changed

.github/actions/spelling/expect.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ Scm
499499
sddl
500500
secureobject
501501
securestring
502+
seekp
502503
seof
503504
servercert
504505
servercertificate
@@ -548,6 +549,7 @@ Tagit
548549
TARG
549550
taskhostw
550551
tcs
552+
tellp
551553
temppath
552554
testexampleinstaller
553555
thiscouldbeapc

doc/Settings.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,35 @@ In addition, there are special values that cover multiple channels. `default` i
286286
},
287287
```
288288

289+
### file
290+
291+
The `file` settings control the log files generated by winget during operation. These settings apply to the automatic cleanup that happens whenever a Windows Package Manager process is run.
292+
They only apply to the default log location, which contains winget logs, AppInstaller logs (the MSIX install UI), and is the default location where installer logs are placed.
293+
The automatic cleanup happens at the beginning of the process, so the log file(s) generated by the current process will not be considered in the limits.
294+
295+
|Setting|Description|Default|Note|
296+
|---|---|---|---|
297+
|`ageLimitInDays`|The maximum age, in days, of files in the log directory; older files are deleted.|7 (days)|Set to 0 to disable this limit.|
298+
|`totalSizeLimitInMB`|The maximum size, in megabytes, of all files in the log directory; the oldest files are deleted first.|128 (MB)|Set to 0 to disable this limit.|
299+
|`countLimit`|The maximum number of files in the log directory; the oldest files are deleted first.|0|Set to 0 (the default) to disable this limit.|
300+
301+
These settings apply to the log files that winget writes, only as they are being written. They do not apply to files written by installers or the AppInstaller UI.
302+
303+
|Setting|Description|Default|Note|
304+
|---|---|---|---|
305+
|`individualSizeLimitInMB`|The maximum size, in megabytes, of an individual log file. If a file would exceed this limit, the logs will wrap. Note that this limit is approximate and the actual files may exceed it by a few bytes.|16 (MB)|Set to 0 to disable this limit.|
306+
307+
```json
308+
"logging": {
309+
"file": {
310+
"ageLimitInDays": 7,
311+
"totalSizeLimitInMB": 128,
312+
"countLimit": 0,
313+
"individualSizeLimitInMB": 16,
314+
}
315+
},
316+
```
317+
289318
## Network
290319

291320
The `network` settings influence how winget uses the network to retrieve packages and metadata.

schemas/JSON/settings/settings.schema.0.2.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,35 @@
8080
},
8181
"minItems": 0,
8282
"maxItems": 20
83+
},
84+
"file": {
85+
"description": "The file settings control the log files generated by winget during operation.",
86+
"type": "object",
87+
"properties": {
88+
"ageLimitInDays": {
89+
"description": "The maximum age, in days, of a log file before it is deleted. Set to 0 to disable automatic deletion based on age.",
90+
"type": "integer",
91+
"default": 7,
92+
"minimum": 0
93+
},
94+
"totalSizeLimitInMB": {
95+
"description": "The maximum total size, in megabytes, of all log files. If the total size exceeds this limit, the oldest files will be deleted first. Set to 0 to disable this limit.",
96+
"type": "integer",
97+
"default": 128,
98+
"minimum": 0
99+
},
100+
"countLimit": {
101+
"description": "The maximum number of log files to retain. If the number of log files exceeds this limit, the oldest files will be deleted first. Set to 0 (the default) to disable this limit.",
102+
"type": "integer",
103+
"default": 0,
104+
"minimum": 0
105+
},
106+
"individualSizeLimitInMB": {
107+
"description": "The maximum size, in megabytes, of an individual log file. If a file would exceed this limit, new log lines will overwrite the file from the beginning. Set to 0 to disable this limit.",
108+
"type": "integer",
109+
"default": 16,
110+
"minimum": 0
111+
}
83112
}
84113
}
85114
},

src/AppInstallerCLITests/AppInstallerCLITests.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
<ClCompile Include="ExperimentalFeature.cpp" />
244244
<ClCompile Include="ExportFlow.cpp" />
245245
<ClCompile Include="FileCache.cpp" />
246+
<ClCompile Include="FileLogger.cpp" />
246247
<ClCompile Include="Filesystem.cpp" />
247248
<ClCompile Include="FolderFileWatcher.cpp" />
248249
<ClCompile Include="FontHelper.cpp" />

src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,9 @@
389389
<ClCompile Include="FontHelper.cpp">
390390
<Filter>Source Files\Repository</Filter>
391391
</ClCompile>
392+
<ClCompile Include="FileLogger.cpp">
393+
<Filter>Source Files\Common</Filter>
394+
</ClCompile>
392395
</ItemGroup>
393396
<ItemGroup>
394397
<None Include="PropertySheet.props" />
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
#include "pch.h"
4+
#include "TestCommon.h"
5+
#include <AppInstallerFileLogger.h>
6+
#include <AppInstallerStrings.h>
7+
8+
using namespace AppInstaller::Logging;
9+
using namespace AppInstaller::Utility;
10+
using namespace TestCommon;
11+
12+
13+
std::string GetHeaderString()
14+
{
15+
return "TIME [CHAN] Header Message";
16+
}
17+
18+
std::string GetLargeString()
19+
{
20+
return "[===|Clearly defined start to large string|===]\r\n"
21+
"While this string does not need to be particularly unique, it is still good if it is not easily duplicated by any other random set of data.\r\n"
22+
"It should also end in a character that is not used in any other way within these tests, so please don't include that character when writing tests.\r\n"
23+
"That character is &";
24+
}
25+
26+
namespace
27+
{
28+
#define WINGET_DEFINE_STRING_ENUM(_enum_,_value_) constexpr std::string_view _enum_##_##_value_ = #_value_##sv
29+
30+
WINGET_DEFINE_STRING_ENUM(TagState, Unset);
31+
WINGET_DEFINE_STRING_ENUM(TagState, SetAtStart);
32+
WINGET_DEFINE_STRING_ENUM(TagState, SetAfterLogging);
33+
34+
WINGET_DEFINE_STRING_ENUM(MaximumSizeState, Zero);
35+
WINGET_DEFINE_STRING_ENUM(MaximumSizeState, SmallerThanLargeString);
36+
WINGET_DEFINE_STRING_ENUM(MaximumSizeState, EqualToLargeString);
37+
WINGET_DEFINE_STRING_ENUM(MaximumSizeState, SlightlyLargerThanLargeString);
38+
WINGET_DEFINE_STRING_ENUM(MaximumSizeState, MuchLargerThanLargeString);
39+
40+
constexpr std::string_view WrapIndicator = "--- log file has wrapped ---"sv;
41+
// The amount of extra size that is allowed (total indicator size + newline + newlines from test strings)
42+
constexpr size_t ExtraAllowedSize = 68;
43+
44+
constexpr size_t NewLineCharacterCount = 2;
45+
constexpr size_t SmallDifferenceSize = 10;
46+
constexpr AppInstaller::Logging::Channel DefaultChannel = AppInstaller::Logging::Channel::Core;
47+
constexpr AppInstaller::Logging::Level DefaultLevel = AppInstaller::Logging::Level::Info;
48+
49+
void ValidateFileContents(const std::filesystem::path& file, const std::vector<std::string_view>& expectedContents, size_t maximumSize)
50+
{
51+
std::ifstream fileStream{ file, std::ios::binary };
52+
auto fileContents = ReadEntireStream(fileStream);
53+
std::string_view fileContentsView = fileContents;
54+
55+
std::string fileContentsCopy = fileContents;
56+
FindAndReplace(fileContentsCopy, "\r", "\\r");
57+
FindAndReplace(fileContentsCopy, "\n", "\\n");
58+
INFO("File contents:\n" << fileContentsCopy);
59+
60+
if (maximumSize)
61+
{
62+
REQUIRE(maximumSize + ExtraAllowedSize >= fileContents.size());
63+
}
64+
65+
size_t currentPosition = 0;
66+
for (std::string_view expectedContent : expectedContents)
67+
{
68+
REQUIRE(currentPosition < fileContents.size());
69+
70+
if (expectedContent == WrapIndicator)
71+
{
72+
auto endLinePosition = fileContentsView.find('\n', currentPosition);
73+
REQUIRE(endLinePosition != -1);
74+
REQUIRE(endLinePosition >= expectedContent.size() + NewLineCharacterCount);
75+
auto actualContent = fileContentsView.substr(endLinePosition + 1 - expectedContent.size() - NewLineCharacterCount, expectedContent.size());
76+
REQUIRE(expectedContent == actualContent);
77+
currentPosition = endLinePosition + 1;
78+
}
79+
else
80+
{
81+
auto actualContent = fileContentsView.substr(currentPosition, expectedContent.size());
82+
REQUIRE(expectedContent == actualContent);
83+
currentPosition += expectedContent.size() + NewLineCharacterCount;
84+
}
85+
}
86+
}
87+
88+
void FileLogger_MaximumSize_Test(std::string_view tagState, std::string_view sizeState)
89+
{
90+
auto headerString = GetHeaderString();
91+
auto largeString = GetLargeString();
92+
93+
// Determine maximum size
94+
size_t maximumSize = 0;
95+
96+
if (sizeState == MaximumSizeState_SmallerThanLargeString)
97+
{
98+
maximumSize = largeString.size() - SmallDifferenceSize;
99+
}
100+
else if (sizeState == MaximumSizeState_EqualToLargeString)
101+
{
102+
maximumSize = largeString.size();
103+
}
104+
else if (sizeState == MaximumSizeState_SlightlyLargerThanLargeString)
105+
{
106+
maximumSize = largeString.size() + SmallDifferenceSize;
107+
}
108+
else if (sizeState == MaximumSizeState_MuchLargerThanLargeString)
109+
{
110+
maximumSize = largeString.size() * 2;
111+
}
112+
113+
INFO("Tag State: " << tagState << ", Size State: " << sizeState << "[" << maximumSize << "]");
114+
115+
TempFile tempFile{ "FileLogger_MaximumSize", ".log" };
116+
FileLogger logger{ tempFile };
117+
118+
INFO("File: " << tempFile.GetPath().u8string());
119+
120+
logger.SetMaximumSize(wil::safe_cast<std::ofstream::off_type>(maximumSize));
121+
122+
// Set tag and log strings
123+
size_t tagPosition = 0;
124+
if (tagState == TagState_SetAtStart)
125+
{
126+
logger.SetTag(Tag::HeadersComplete);
127+
}
128+
129+
logger.WriteDirect(DefaultChannel, DefaultLevel, headerString);
130+
131+
if (tagState == TagState_SetAfterLogging)
132+
{
133+
logger.SetTag(Tag::HeadersComplete);
134+
tagPosition = headerString.size() + NewLineCharacterCount;
135+
}
136+
137+
// Due to text output in the logger, log with \n only
138+
std::string largeStringWithoutCarriageReturn = largeString;
139+
FindAndReplace(largeStringWithoutCarriageReturn, "\r\n", "\n");
140+
141+
logger.WriteDirect(DefaultChannel, DefaultLevel, largeStringWithoutCarriageReturn);
142+
143+
// Calculate current state
144+
size_t maximumAvailableSpace = std::numeric_limits<size_t>::max();
145+
size_t currentAvailableSpace = std::numeric_limits<size_t>::max();
146+
if (maximumSize)
147+
{
148+
maximumAvailableSpace = maximumSize - tagPosition;
149+
currentAvailableSpace = maximumSize - headerString.size() - NewLineCharacterCount;
150+
}
151+
152+
bool shouldWrap = largeString.size() > currentAvailableSpace;
153+
154+
INFO("Maximum Available: " << maximumAvailableSpace << ", Current Available: " << currentAvailableSpace << ", ShouldWrap: " << shouldWrap);
155+
156+
std::vector<std::string_view> expectedFileContents;
157+
158+
if (tagPosition || !shouldWrap)
159+
{
160+
expectedFileContents.push_back(headerString);
161+
}
162+
163+
if (shouldWrap)
164+
{
165+
expectedFileContents.push_back(WrapIndicator);
166+
}
167+
168+
std::string_view largeStringView = largeString;
169+
expectedFileContents.push_back(largeStringView.substr(0, std::min(largeString.size(), maximumAvailableSpace)));
170+
171+
ValidateFileContents(tempFile, expectedFileContents, maximumSize);
172+
173+
// Log again
174+
INFO("Second time logging large string");
175+
logger.WriteDirect(DefaultChannel, DefaultLevel, largeStringWithoutCarriageReturn);
176+
177+
// The maximum size is twice the large log, so anything with a limit will wrap
178+
shouldWrap = maximumSize != 0;
179+
180+
expectedFileContents.clear();
181+
182+
if (tagPosition || !shouldWrap)
183+
{
184+
expectedFileContents.push_back(headerString);
185+
}
186+
187+
if (shouldWrap)
188+
{
189+
expectedFileContents.push_back(WrapIndicator);
190+
}
191+
else
192+
{
193+
expectedFileContents.push_back(largeStringView);
194+
}
195+
196+
expectedFileContents.push_back(largeStringView.substr(0, std::min(largeString.size(), maximumAvailableSpace)));
197+
198+
ValidateFileContents(tempFile, expectedFileContents, maximumSize);
199+
}
200+
}
201+
202+
TEST_CASE("FileLogger_MaximumSize", "[logging]")
203+
{
204+
auto tagState = GENERATE(TagState_Unset, TagState_SetAtStart, TagState_SetAfterLogging);
205+
auto sizeState = GENERATE(MaximumSizeState_Zero, MaximumSizeState_SmallerThanLargeString, MaximumSizeState_EqualToLargeString, MaximumSizeState_SlightlyLargerThanLargeString, MaximumSizeState_MuchLargerThanLargeString);
206+
FileLogger_MaximumSize_Test(tagState, sizeState);
207+
}
208+
209+
TEST_CASE("FileLogger_MaximumSize_ManyWraps", "[logging]")
210+
{
211+
TempFile tempFile{ "FileLogger_ManyWraps", ".log" };
212+
FileLogger logger{ tempFile };
213+
214+
INFO("File: " << tempFile.GetPath().u8string());
215+
216+
size_t maximumSize = 1000;
217+
logger.SetMaximumSize(static_cast<std::ofstream::off_type>(maximumSize));
218+
219+
std::string header = GetHeaderString();
220+
header += " !Now with more header!";
221+
std::string largeString = "[*=INIT=*]Now we just need another few dozen characters, which shouldn't be that hard to get. Wow, made it already.";
222+
std::string_view largeStringView = largeString;
223+
size_t initSize = 10;
224+
225+
logger.WriteDirect(DefaultChannel, DefaultLevel, header);
226+
logger.SetTag(Tag::HeadersComplete);
227+
228+
// Use the default seed value as we want arbitrary but reproducible results
229+
std::default_random_engine randomEngine;
230+
std::uniform_int_distribution<> sizeDistribution(static_cast<int>(initSize), 100);
231+
232+
// We should expect ~500 wraps on average
233+
for (size_t i = 0; i < 9999; ++i)
234+
{
235+
logger.WriteDirect(DefaultChannel, DefaultLevel, largeStringView.substr(0, sizeDistribution(randomEngine)));
236+
}
237+
238+
// We want the header to be preserved, followed by the wrap indicator, and at a minimum we should see the first few characters in the log string
239+
std::vector<std::string_view> expectedFileContents;
240+
expectedFileContents.push_back(header);
241+
expectedFileContents.push_back(WrapIndicator);
242+
expectedFileContents.push_back(largeStringView.substr(0, initSize));
243+
244+
ValidateFileContents(tempFile, expectedFileContents, maximumSize);
245+
}

0 commit comments

Comments
 (0)