Skip to content

Commit d7d76d3

Browse files
committed
cvd: prune old log files in user log directory
- Introduces PruneLogsDirectory to keep only the 30 most recent log files in PerUserDir() + "/logs". - Refactors log initialization in main.cc to call this pruning function during startup. - Refactors GetCvdLogFile into GetCvdLogFileName returning std::string. - Adds a test for PruneLogsDirectory. Assisted-by: Jetski <jetski@google.com> Bug: b/507600500
1 parent 9381f07 commit d7d76d3

5 files changed

Lines changed: 179 additions & 30 deletions

File tree

base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,29 @@ cf_cc_library(
5656
name = "log_files",
5757
srcs = ["log_files.cpp"],
5858
hdrs = ["log_files.h"],
59+
clang_format_enabled = True,
5960
deps = [
6061
"//cuttlefish/common/libs/utils:files",
6162
"//cuttlefish/host/commands/cvd/utils",
63+
"//cuttlefish/result",
64+
"@abseil-cpp//absl/log",
6265
"@abseil-cpp//absl/strings",
6366
],
6467
)
6568

69+
cf_cc_test(
70+
name = "log_files_test",
71+
srcs = ["log_files_test.cpp"],
72+
clang_format_enabled = True,
73+
deps = [
74+
"//cuttlefish/common/libs/utils:files",
75+
"//cuttlefish/host/commands/cvd/cli:log_files",
76+
"//cuttlefish/host/commands/cvd/utils",
77+
"//cuttlefish/result:result_matchers",
78+
"@abseil-cpp//absl/cleanup",
79+
],
80+
)
81+
6682
# Commands that may invoke other commands
6783
cf_cc_library(
6884
name = "nesting_commands",

base/cvd/cuttlefish/host/commands/cvd/cli/log_files.cpp

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,40 @@
1616

1717
#include "cuttlefish/host/commands/cvd/cli/log_files.h"
1818

19+
#include <algorithm>
1920
#include <chrono>
2021
#include <iomanip>
2122
#include <iostream>
2223
#include <sstream>
2324
#include <string>
2425
#include <vector>
2526

26-
#include "absl/strings/str_cat.h"
27+
#include "absl/log/log.h"
28+
#include "absl/strings/match.h"
2729

2830
#include "cuttlefish/common/libs/utils/files.h"
2931
#include "cuttlefish/host/commands/cvd/utils/common.h"
3032

3133
namespace cuttlefish {
34+
namespace {
3235

33-
std::optional<std::string> GetCvdLogFile() {
34-
std::string log_dir = PerUserDir() + "/logs";
35-
if (!EnsureDirectoryExists(log_dir, 0777).ok()) {
36-
std::cerr << "Failed to create log directory: " << log_dir
37-
<< ". Logging to file disabled." << std::endl;
38-
return std::nullopt;
39-
}
36+
constexpr std::string_view kLogFilePrefix = "cvd_";
37+
constexpr std::string_view kLogFileSuffix = ".log";
38+
39+
bool IsNotLogFile(std::string_view name) {
40+
const bool prefix_match = absl::StartsWith(name, kLogFilePrefix);
41+
const bool suffix_match = absl::EndsWith(name, kLogFileSuffix);
42+
return !(prefix_match && suffix_match);
43+
}
44+
45+
} // namespace
4046

41-
std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
42-
std::time_t now_time_t = std::chrono::system_clock::to_time_t(now);
43-
std::chrono::milliseconds now_ms =
47+
std::string CvdUserLogDir() { return PerUserDir() + "/logs"; }
48+
49+
std::string GetCvdLogFileName(const std::string& log_dir,
50+
const std::chrono::system_clock::time_point now) {
51+
const std::time_t now_time_t = std::chrono::system_clock::to_time_t(now);
52+
const std::chrono::milliseconds now_ms =
4453
std::chrono::duration_cast<std::chrono::milliseconds>(
4554
now.time_since_epoch()) %
4655
1000;
@@ -49,9 +58,35 @@ std::optional<std::string> GetCvdLogFile() {
4958
localtime_r(&now_time_t, &tm_now);
5059

5160
std::stringstream ss;
52-
ss << std::put_time(&tm_now, "%Y%m%d_%H%M%S") << "." << std::setfill('0')
53-
<< std::setw(3) << now_ms.count();
54-
return absl::StrCat(log_dir, "/cvd_", ss.str(), ".log");
61+
ss << log_dir << "/" << kLogFilePrefix
62+
<< std::put_time(&tm_now, "%Y%m%d_%H%M%S") << "." << std::setfill('0')
63+
<< std::setw(3) << now_ms.count() << kLogFileSuffix;
64+
return ss.str();
65+
}
66+
67+
Result<void> PruneLogsDirectory(const std::string& log_dir, size_t retain) {
68+
if (!DirectoryExists(log_dir)) {
69+
return {};
70+
}
71+
std::vector<std::string> log_files =
72+
CF_EXPECTF(DirectoryContents(log_dir),
73+
"Failed to list log directory: '{}'", log_dir);
74+
size_t non_log_files = std::erase_if(log_files, IsNotLogFile);
75+
if (non_log_files > 0) {
76+
VLOG(0) << non_log_files << " non-log files found.";
77+
}
78+
79+
std::sort(log_files.begin(), log_files.end());
80+
81+
if (log_files.size() <= retain) {
82+
return {};
83+
}
84+
for (size_t i = 0; i < log_files.size() - retain; ++i) {
85+
const std::string file_to_remove = log_dir + "/" + log_files[i];
86+
CF_EXPECTF(RemoveFile(file_to_remove),
87+
"Failed to remove old log file: '{}'", file_to_remove);
88+
}
89+
return {};
5590
}
5691

5792
} // namespace cuttlefish

base/cvd/cuttlefish/host/commands/cvd/cli/log_files.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,30 @@
1616

1717
#pragma once
1818

19-
#include <optional>
19+
#include <chrono>
2020
#include <string>
2121

22+
#include "cuttlefish/result/result.h"
23+
2224
namespace cuttlefish {
2325

26+
inline constexpr size_t kCvdRetainLogFilesNum = 30;
27+
28+
/** A user-specific directory where log files from `cvd` are stored. */
29+
std::string CvdUserLogDir();
30+
2431
/**
2532
* Returns the path to a new timestamped log file for the
2633
* cvd command. The log file is created in PerUserDir() + "/logs/" with a name
2734
* like cvd_YYYYMMDD_HHMMSS.ms.log. If the logs directory cannot be created, the
2835
* function returns std::nullopt.
2936
*/
30-
std::optional<std::string> GetCvdLogFile();
37+
std::string GetCvdLogFileName(const std::string& log_dir = CvdUserLogDir(),
38+
std::chrono::system_clock::time_point now =
39+
std::chrono::system_clock::now());
40+
41+
/** Prunes the logs directory keeping only the most recent log files. */
42+
Result<void> PruneLogsDirectory(const std::string& log_dir = CvdUserLogDir(),
43+
size_t retain = kCvdRetainLogFilesNum);
3144

3245
} // namespace cuttlefish
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (C) 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "cuttlefish/host/commands/cvd/cli/log_files.h"
18+
19+
#include <algorithm>
20+
#include <chrono>
21+
#include <cstdlib>
22+
#include <string>
23+
#include <vector>
24+
25+
#include "absl/cleanup/cleanup.h"
26+
#include "gmock/gmock.h"
27+
#include "gtest/gtest.h"
28+
29+
#include "cuttlefish/common/libs/utils/files.h"
30+
#include "cuttlefish/result/result_matchers.h"
31+
32+
namespace cuttlefish {
33+
namespace {
34+
35+
TEST(LogFilesTest, PruneLogsDirectory) {
36+
const char* tmpdir = getenv("TEST_TMPDIR");
37+
ASSERT_NE(tmpdir, nullptr);
38+
std::string log_dir = std::string(tmpdir) + "/logs_XXXXXX";
39+
ASSERT_NE(mkdtemp(log_dir.data()), nullptr);
40+
41+
absl::Cleanup cleanup = [log_dir]() {
42+
(void)RecursivelyRemoveDirectory(log_dir);
43+
};
44+
45+
// Create 35 log files
46+
const std::chrono::system_clock::time_point base_time =
47+
std::chrono::system_clock::now();
48+
std::vector<std::string> expected_logs;
49+
for (int i = 1; i <= 35; ++i) {
50+
const std::chrono::system_clock::time_point fake_time =
51+
base_time + std::chrono::seconds(i);
52+
const std::string path = GetCvdLogFileName(log_dir, fake_time);
53+
ASSERT_THAT(WriteNewFile(path, "content"), IsOk());
54+
if (i >= 6) {
55+
expected_logs.push_back(path.substr(log_dir.size() + 1));
56+
}
57+
}
58+
59+
// Also create one non-log file to ensure it's not pruned
60+
const std::string non_log = log_dir + "/not_a_log.txt";
61+
ASSERT_THAT(WriteNewFile(non_log, "content"), IsOk());
62+
63+
ASSERT_THAT(PruneLogsDirectory(log_dir, 30), IsOk());
64+
65+
Result<std::vector<std::string>> remaining = DirectoryContents(log_dir);
66+
ASSERT_THAT(remaining, IsOk());
67+
68+
EXPECT_TRUE(FileExists(non_log));
69+
std::erase(*remaining, "not_a_log.txt");
70+
71+
EXPECT_EQ(remaining->size(), 30);
72+
// Verify the remaining ones are the newest (i from 6 to 35)
73+
std::sort(remaining->begin(), remaining->end());
74+
std::sort(expected_logs.begin(), expected_logs.end());
75+
EXPECT_EQ(*remaining, expected_logs);
76+
}
77+
78+
} // namespace
79+
} // namespace cuttlefish

base/cvd/cuttlefish/host/commands/cvd/main.cc

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -199,29 +199,35 @@ std::string ColoredUrl(const std::string& url) {
199199
return output;
200200
}
201201

202+
void InitializeLogs(std::vector<std::string>& all_args) {
203+
(void)PruneLogsDirectory(CvdUserLogDir());
204+
205+
LogSeverity verbosity = CvdVerbosityOption(all_args);
206+
MetadataLevel metadata_level =
207+
isatty(0) ? MetadataLevel::ONLY_MESSAGE : MetadataLevel::FULL;
208+
209+
std::vector<std::string> log_files;
210+
if (EnsureDirectoryExists(CvdUserLogDir(), 0777).ok()) {
211+
log_files.push_back(GetCvdLogFileName(CvdUserLogDir()));
212+
} else {
213+
std::cerr << "File logging disabled, could not create " << CvdUserLogDir()
214+
<< std::endl;
215+
}
216+
LogToStderrAndFiles(log_files, "", metadata_level, verbosity);
217+
}
218+
202219
} // namespace
203220

204221
} // namespace cuttlefish
205222

206223
int main(int argc, char** argv) {
207224
srand(time(NULL));
208225

209-
cuttlefish::cvd_common::Args all_args = cuttlefish::ArgsToVec(argc, argv);
210-
cuttlefish::LogSeverity verbosity =
211-
cuttlefish::CvdVerbosityOption(all_args);
212-
// set verbosity for this process
213-
cuttlefish::MetadataLevel metadata_level =
214-
isatty(0) ? cuttlefish::MetadataLevel::ONLY_MESSAGE
215-
: cuttlefish::MetadataLevel::FULL;
226+
std::vector<std::string> all_args = cuttlefish::ArgsToVec(argc, argv);
216227

217-
std::optional<std::string> log_file = cuttlefish::GetCvdLogFile();
218-
std::vector<std::string> log_files;
219-
if (log_file) {
220-
log_files.push_back(*log_file);
221-
}
222-
cuttlefish::LogToStderrAndFiles(log_files, "", metadata_level, verbosity);
228+
cuttlefish::InitializeLogs(all_args);
223229

224-
auto result = cuttlefish::CvdMain(std::move(all_args));
230+
cuttlefish::Result<void> result = cuttlefish::CvdMain(std::move(all_args));
225231
if (result.ok()) {
226232
return 0;
227233
} else {

0 commit comments

Comments
 (0)