Skip to content

Commit 7355091

Browse files
committed
feat components: boot logger
commit_hash:186dacb5820cf5cd80f98b15b723b0bc2924b868
1 parent e296c82 commit 7355091

13 files changed

Lines changed: 295 additions & 19 deletions

File tree

.mapping.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,7 @@
665665
"core/functional_tests/basic_chaos/tests-nonchaos/handlers/test_log_request_headers.py":"taxi/uservices/userver/core/functional_tests/basic_chaos/tests-nonchaos/handlers/test_log_request_headers.py",
666666
"core/functional_tests/basic_chaos/tests-nonchaos/metrics/test_has_reopening_error_metric.py":"taxi/uservices/userver/core/functional_tests/basic_chaos/tests-nonchaos/metrics/test_has_reopening_error_metric.py",
667667
"core/functional_tests/basic_chaos/tests-nonchaos/middlewares/test_testsuite_middleware.py":"taxi/uservices/userver/core/functional_tests/basic_chaos/tests-nonchaos/middlewares/test_testsuite_middleware.py",
668+
"core/functional_tests/basic_chaos/tests-nonchaos/test_boot_log.py":"taxi/uservices/userver/core/functional_tests/basic_chaos/tests-nonchaos/test_boot_log.py",
668669
"core/functional_tests/basic_chaos/tests-nonchaos/test_dynamic_debug_log.py":"taxi/uservices/userver/core/functional_tests/basic_chaos/tests-nonchaos/test_dynamic_debug_log.py",
669670
"core/functional_tests/basic_chaos/tests-restart/conftest.py":"taxi/uservices/userver/core/functional_tests/basic_chaos/tests-restart/conftest.py",
670671
"core/functional_tests/basic_chaos/tests-restart/test_restart.py":"taxi/uservices/userver/core/functional_tests/basic_chaos/tests-restart/test_restart.py",
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import json
2+
3+
import pytest
4+
5+
from testsuite.daemons import spawn
6+
7+
8+
def default_stderr_handler(line: str):
9+
pass
10+
11+
12+
@pytest.fixture(scope='session')
13+
def boot_log_path(service_tmpdir):
14+
return str(service_tmpdir / 'boot.log')
15+
16+
17+
@pytest.fixture(scope='session')
18+
def enable_boot_logger(boot_log_path):
19+
def patch_config(config_yaml, _config_vars) -> None:
20+
config_yaml['components_manager']['boot-log-path'] = boot_log_path
21+
22+
return patch_config
23+
24+
25+
@pytest.fixture(scope='session')
26+
def break_logger_config(boot_log_path):
27+
def patch_config(config_yaml, _config_vars) -> None:
28+
config_yaml['components_manager']['components']['logging']['loggers']['default']['file_path'] = '/'
29+
30+
return patch_config
31+
32+
33+
@pytest.fixture
34+
def assert_service_boot_fails(
35+
service_binary,
36+
service_config_path_temp,
37+
service_env,
38+
create_daemon_scope,
39+
ensure_daemon_started,
40+
service_http_ping_url,
41+
):
42+
async def func(stderr_handler=default_stderr_handler):
43+
async with create_daemon_scope(
44+
args=[
45+
str(service_binary),
46+
'--config',
47+
str(service_config_path_temp),
48+
],
49+
env=service_env,
50+
ping_url=service_http_ping_url,
51+
stderr_handler=stderr_handler,
52+
) as scope:
53+
with pytest.raises((spawn.HealthCheckError, spawn.ExitCodeError)):
54+
await ensure_daemon_started(scope)
55+
56+
return func
57+
58+
59+
def read_boot_log(path: str) -> list[dict[str, str | int]]:
60+
with open(path) as ifile:
61+
content = ifile.read()
62+
63+
logs = []
64+
for line in content.splitlines():
65+
line = line.strip()
66+
if not line:
67+
continue
68+
69+
try:
70+
logs.append(json.loads(line))
71+
except BaseException:
72+
pytest.fail('Invalid JSON string in bool log: {line}')
73+
return logs
74+
75+
76+
@pytest.mark.uservice_oneshot(config_hooks=['enable_boot_logger'])
77+
async def test_boot_ok(service_client, boot_log_path):
78+
logs = read_boot_log(boot_log_path)
79+
80+
assert logs[0]['event_type'] == 'component_system_is_starting'
81+
82+
components = list(
83+
map(lambda x: x['component_name'], filter(lambda x: x['event_type'] == 'component_is_starting', logs))
84+
)
85+
86+
checked_components = {
87+
'dns-client',
88+
'http-client',
89+
'logging',
90+
'logging-configurator',
91+
'manager-controller',
92+
'os-signal-processor',
93+
'server',
94+
'statistics-storage',
95+
'tests-control',
96+
'testsuite-support',
97+
}
98+
assert checked_components.issubset(components)
99+
100+
assert logs[-1]['event_type'] == 'component_system_is_started'
101+
102+
103+
@pytest.mark.uservice_oneshot(config_hooks=['enable_boot_logger', 'break_logger_config'])
104+
async def test_boot_fail(
105+
assert_service_boot_fails,
106+
boot_log_path,
107+
):
108+
await assert_service_boot_fails()
109+
110+
logs = read_boot_log(boot_log_path)
111+
logging_events = list(filter(lambda x: x.get('component_name') == 'logging', logs))
112+
113+
# 'logging' component is at least created
114+
assert logging_events[0]['event_type'] == 'component_is_starting'
115+
116+
# Boot failure may lead to crash, so the last log item event_type is anything, but 'component_system_is_started'
117+
assert logs[-1]['event_type'] != 'component_system_is_started'
118+
119+
120+
@pytest.fixture(scope='session')
121+
def unix_socket_logger_with_truncate():
122+
def patch_config(config_yaml, _config_vars) -> None:
123+
config_yaml['components_manager']['components']['logging']['loggers']['unix'] = {
124+
'file_path': 'unix:/path',
125+
'truncate-on-start': True,
126+
}
127+
128+
return patch_config
129+
130+
131+
@pytest.mark.uservice_oneshot(config_hooks=['unix_socket_logger_with_truncate'])
132+
async def test_unix_socket_with_truncate(
133+
assert_service_boot_fails,
134+
_service_logfile_path,
135+
):
136+
stderr = bytes()
137+
138+
def handler(line: bytes):
139+
nonlocal stderr
140+
stderr += line
141+
142+
await assert_service_boot_fails(stderr_handler=handler)
143+
144+
expected_msg = (
145+
"Cannot start component logging: truncate-on-start cannot be combined with unix socket path for logger 'unix'"
146+
)
147+
assert expected_msg in stderr.decode('utf-8')

core/functional_tests/signal_during_boot/tests/test_rotate_logs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ async def check_ready():
1717
await sync.wait_until(check_ready)
1818

1919

20+
def stderr_handler(line: str):
21+
pass
22+
23+
2024
async def test_boot_logs(
2125
service_binary,
2226
service_config_path_temp,
@@ -36,6 +40,7 @@ async def _checker(*, session, process) -> bool:
3640
],
3741
health_check=_checker,
3842
env=service_env,
43+
stderr_handler=stderr_handler,
3944
) as scope:
4045
daemon = await ensure_daemon_started(scope)
4146

core/src/components/component_context_impl.cpp

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <components/manager.hpp>
1919
#include <components/manager_config.hpp>
2020
#include <engine/task/task_processor.hpp>
21+
#include <logging/tp_logger_utils.hpp>
2122

2223
USERVER_NAMESPACE_BEGIN
2324

@@ -96,6 +97,21 @@ ComponentContextImpl::ComponentContextImpl(const Manager& manager, std::vector<s
9697
UINVARIANT(success, "Duplicate component names");
9798
}
9899

100+
if (!manager_.GetConfig().boot_log_path.empty()) {
101+
logging::LoggerConfig config;
102+
config.logger_name = "boot";
103+
config.file_path = manager_.GetConfig().boot_log_path;
104+
config.level = logging::Level::kTrace;
105+
config.format = logging::Format::kJson;
106+
config.truncate_on_start = true;
107+
config.queue_overflow_behavior = logging::QueueOverflowBehavior::kBlock;
108+
109+
boot_logger_ = logging::impl::MakeTpLogger(config);
110+
}
111+
112+
LOG_DEBUG() << "Starting component system";
113+
EmitBootEvent(Event::kComponentSystemIsStarting);
114+
99115
StartPrintAddingComponentsTask();
100116
}
101117

@@ -104,6 +120,9 @@ RawComponentBase* ComponentContextImpl::AddComponent(
104120
const ComponentConfig& config,
105121
const ComponentAdderBase& adder
106122
) {
123+
LOG_DEBUG() << "Starting component " << name;
124+
EmitBootEvent(Event::kStarting, {{"component_name", name}});
125+
107126
auto& component_info = GetComponentInfo(*this, name);
108127
const LoadingComponentScope loading_component_scope(*this, component_info);
109128

@@ -125,6 +144,10 @@ RawComponentBase* ComponentContextImpl::AddComponent(
125144
<< fmt::format("\"{0}\" [label=\"{0}\n{1}\"]; ", name, compiler::GetTypeName(typeid(*component)))
126145
<< component_info.GetDependencies();
127146
}
147+
148+
LOG_DEBUG() << "Started component " << name;
149+
EmitBootEvent(Event::kStarted, {{"component_name", name}});
150+
128151
return component;
129152
}
130153

@@ -143,6 +166,9 @@ void ComponentContextImpl::OnAllComponentsLoaded() {
143166

144167
// In case no exception flies out, the service is fully loaded at this point.
145168
service_lifetime_stage_ = ServiceLifetimeStage::kRunning;
169+
170+
LOG_INFO() << "All components loaded";
171+
EmitBootEvent(Event::kComponentSystemIsStarted);
146172
}
147173

148174
void ComponentContextImpl::OnGracefulShutdownStarted() {
@@ -372,6 +398,14 @@ void ComponentContextImpl::ProcessSingleComponentLifetimeStageSwitching(
372398
}
373399

374400
LOG_DEBUG() << "Call " << params.stage_switch_handler_name << " for component '" << name << "'";
401+
EmitBootEvent(
402+
Event::kSwitching,
403+
{
404+
{"component_name", name},
405+
{"stage_switch_handler_name", params.stage_switch_handler_name},
406+
}
407+
);
408+
375409
(component_info.*params.stage_switch_handler)();
376410
} catch (const impl::StageSwitchingCancelledException& ex) {
377411
LOG_WARNING() << params.stage_switch_handler_name << " failed for component '" << name << "': " << ex;
@@ -442,6 +476,13 @@ RawComponentBase* ComponentContextImpl::DoFindComponent(std::string_view name, C
442476
LOG_DEBUG()
443477
<< "Component '" << name << "' is not loaded yet, component '" << current_component.GetName()
444478
<< "' is waiting for it to load";
479+
EmitBootEvent(
480+
Event::kWaitingForComponent,
481+
{
482+
{"component_name", current_component.GetName()},
483+
{"dependency_name", name},
484+
}
485+
);
445486

446487
const SearchingComponentScope finder(*this, current_component);
447488

@@ -456,6 +497,14 @@ void ComponentContextImpl::AddDependency(ComponentInfo& dependency, ComponentInf
456497
}
457498

458499
LOG_DEBUG() << "Resolving dependency " << current_component.GetName() << " -> " << dependency.GetName();
500+
EmitBootEvent(
501+
Event::kFindComponent,
502+
{
503+
{"component_name", current_component.GetName()},
504+
{"dependency_name", dependency.GetName()},
505+
}
506+
);
507+
459508
CheckForDependencyCycle(current_component, dependency, *data);
460509

461510
current_component.AddItDependsOn(dependency);
@@ -522,6 +571,7 @@ void ComponentContextImpl::PrepareComponentLifetimeStageSwitching() {
522571
}
523572

524573
void ComponentContextImpl::CancelComponentLifetimeStageSwitching() {
574+
EmitBootEvent(Event::kLoadingIsCancelled);
525575
for (auto& component_item : components_) {
526576
component_item->SetStageSwitchingCancelled(true);
527577
}
@@ -580,6 +630,41 @@ void ComponentContextImpl::PrintAddingComponents() const {
580630
trace_plugin_.PrintStacksByComponentNames(ExtractNamesFromInfo(busy_components));
581631
}
582632

633+
std::string_view ComponentContextImpl::ToString(Event event)
634+
{
635+
switch (event) {
636+
case Event::kComponentSystemIsStarting:
637+
return "component_system_is_starting";
638+
case Event::kStarting:
639+
return "component_is_starting";
640+
case Event::kStarted:
641+
return "component_is_created";
642+
case Event::kComponentSystemIsStarted:
643+
return "component_system_is_started";
644+
case Event::kLoadingIsCancelled:
645+
return "loading_is_cancelled";
646+
case Event::kSwitching:
647+
return "component_is_switching_stage";
648+
case Event::kWaitingForComponent:
649+
return "waiting_for_component";
650+
case Event::kFindComponent:
651+
return "find_component";
652+
}
653+
}
654+
655+
void ComponentContextImpl::EmitBootEvent(Event event, logging::LogExtra log_extra) const {
656+
if (!boot_logger_) {
657+
return;
658+
}
659+
660+
log_extra.Extend("event_type", std::string{ToString(event)});
661+
LOG_INFO_TO(*boot_logger_) << log_extra;
662+
663+
if (event == Event::kComponentSystemIsStarted) {
664+
boot_logger_->Flush();
665+
}
666+
}
667+
583668
} // namespace components::impl
584669

585670
USERVER_NAMESPACE_END

core/src/components/component_context_impl.hpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,22 @@ class ComponentContextImpl {
175175
void StopPrintAddingComponentsTask();
176176
void PrintAddingComponents() const;
177177

178+
enum class Event {
179+
kComponentSystemIsStarting,
180+
kStarting,
181+
kStarted,
182+
kComponentSystemIsStarted,
183+
kLoadingIsCancelled,
184+
kSwitching,
185+
kWaitingForComponent,
186+
kFindComponent,
187+
};
188+
static std::string_view ToString(Event event);
189+
190+
void EmitBootEvent(Event event, logging::LogExtra log_extra = {}) const;
191+
178192
const Manager& manager_;
193+
logging::LoggerPtr boot_logger_;
179194

180195
ComponentMap components_;
181196
std::atomic_flag components_load_cancelled_ ATOMIC_FLAG_INIT;

core/src/components/manager.cpp

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -440,8 +440,6 @@ void Manager::AddComponents(const ComponentList& component_list) {
440440

441441
auto stop_time = std::chrono::steady_clock::now();
442442
load_duration_ = std::chrono::duration_cast<std::chrono::milliseconds>(stop_time - start_time);
443-
444-
LOG_INFO() << "All components loaded";
445443
}
446444

447445
void Manager::AddComponentImpl(
@@ -459,14 +457,11 @@ void Manager::AddComponentImpl(
459457
return;
460458
}
461459

462-
LOG_DEBUG() << "Starting component " << name;
463-
464460
auto* component = component_context_->AddComponent(name, config_it->second, adder);
465461
if (auto* signal_processor = dynamic_cast<os_signals::ProcessorComponent*>(component)) {
466462
const std::lock_guard<std::shared_timed_mutex> lock(context_mutex_);
467463
signal_processor_ = signal_processor;
468464
}
469-
LOG_DEBUG() << "Started component " << name;
470465
}
471466

472467
void Manager::ClearComponents() noexcept {

core/src/components/manager_config.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ additionalProperties: false
143143
description: 'dictionary of "component name": "options"'
144144
additionalProperties: true
145145
properties: {}
146+
boot-log-path:
147+
type: string
148+
description: if non-empty, defines path to boot logs
146149
task_processors:
147150
type: object
148151
description: dictionary of task processors to create and their options
@@ -293,6 +296,7 @@ ManagerConfig Parse(const yaml_config::YamlConfig& value, formats::parse::To<Man
293296
);
294297
}
295298
config.components = yaml_config::ParseMapToArray<components::ComponentConfig>(value["components"]);
299+
config.boot_log_path = value["boot-log-path"].As<std::string>({});
296300
config.task_processors = yaml_config::ParseMapToArray<engine::TaskProcessorConfig>(value["task_processors"]);
297301
config.default_task_processor = value["default_task_processor"].As<std::string>("main-task-processor");
298302
config.fs_task_processor = value["fs_task_processor"].As<std::string>("fs-task-processor");

0 commit comments

Comments
 (0)