1 : // Copyright 2012 Google Inc. All Rights Reserved.
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 : //
15 : // This file defines the trace::agent_logger::Logger class which implements the
16 : // Logger RPC interface.
17 :
18 : #include "syzygy/trace/agent_logger/agent_logger.h"
19 :
20 : #include <windows.h> // NOLINT
21 : #include <dbghelp.h>
22 : #include <psapi.h>
23 :
24 : #include "base/bind.h"
25 : #include "base/strings/string_util.h"
26 : #include "base/strings/stringprintf.h"
27 : #include "base/win/scoped_handle.h"
28 : #include "syzygy/common/com_utils.h"
29 : #include "syzygy/common/dbghelp_util.h"
30 : #include "syzygy/common/rpc/helpers.h"
31 : #include "syzygy/kasko/minidump.h"
32 : #include "syzygy/kasko/minidump_request.h"
33 : #include "syzygy/kasko/api/client.h"
34 : #include "syzygy/pe/find.h"
35 :
36 : namespace trace {
37 : namespace agent_logger {
38 :
39 : namespace {
40 :
41 : using ::common::rpc::GetInstanceString;
42 :
43 : // A helper class to manage a SYMBOL_INFO structure.
44 : template <size_t max_name_len>
45 : class SymbolInfo {
46 : public:
47 E : SymbolInfo() {
48 : static_assert(max_name_len > 0, "Maximum name length should be > 0.");
49 : static_assert(
50 : sizeof(buf_) - sizeof(info_) >= max_name_len * sizeof(wchar_t),
51 : "Not enough buffer space for max_name_len wchars");
52 :
53 E : ::memset(buf_, 0, sizeof(buf_));
54 E : info_.SizeOfStruct = sizeof(info_);
55 E : info_.MaxNameLen = max_name_len;
56 E : }
57 :
58 E : PSYMBOL_INFO Get() { return &info_; }
59 :
60 E : PSYMBOL_INFO operator->() { return &info_; }
61 :
62 : private:
63 : // SYMBOL_INFO is a variable length structure ending with a string (the
64 : // name of the symbol). The SYMBOL_INFO struct itself only declares the
65 : // first byte of the Name array, the rest we reserve by holding it in
66 : // union with a properly sized underlying buffer.
67 : union {
68 : SYMBOL_INFO info_;
69 : char buf_[sizeof(SYMBOL_INFO) + max_name_len * sizeof(wchar_t)];
70 : };
71 : };
72 :
73 : void GetSymbolInfo(HANDLE process,
74 : DWORD frame_ptr,
75 : std::string* name,
76 E : DWORD64* offset) {
77 E : DCHECK(frame_ptr != NULL);
78 E : DCHECK(name != NULL);
79 E : DCHECK(offset != NULL);
80 :
81 : // Constants we'll need later.
82 : static const size_t kMaxNameLength = 256;
83 E : SymbolInfo<kMaxNameLength> symbol;
84 :
85 : // Lookup the symbol by address.
86 E : if (::SymFromAddr(process, frame_ptr, offset, symbol.Get())) {
87 E : base::SStringPrintf(name, "%s+%ld", symbol->Name, *offset);
88 E : } else {
89 E : base::SStringPrintf(name, "(unknown)+%ld", *offset);
90 : }
91 E : }
92 :
93 E : void GetLineInfo(HANDLE process, DWORD_PTR frame, std::string* line_info) {
94 E : DCHECK(frame != NULL);
95 E : DCHECK(line_info != NULL);
96 :
97 E : DWORD line_displacement = 0;
98 E : IMAGEHLP_LINE64 line = {};
99 E : line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
100 E : if (::SymGetLineFromAddr64(process, frame, &line_displacement, &line)) {
101 E : base::SStringPrintf(line_info, "%s:%d", line.FileName, line.LineNumber);
102 E : } else {
103 E : line_info->clear();
104 : }
105 E : }
106 :
107 : // A callback function used with the StackWalk64 function. It is called when
108 : // StackWalk64 needs to read memory from the address space of the process.
109 : // http://msdn.microsoft.com/en-us/library/windows/desktop/ms680559.aspx
110 : BOOL CALLBACK ReadProcessMemoryProc64(HANDLE process,
111 : DWORD64 base_address_64,
112 : PVOID buffer,
113 : DWORD size,
114 E : LPDWORD bytes_read) {
115 E : DCHECK(buffer != NULL);
116 E : DCHECK(bytes_read != NULL);
117 E : *bytes_read = 0;
118 E : LPCVOID base_address = reinterpret_cast<LPCVOID>(base_address_64);
119 E : if (::ReadProcessMemory(process, base_address, buffer, size, bytes_read))
120 E : return TRUE;
121 :
122 : // Maybe it was just a partial read, which isn't fatal.
123 E : DWORD error = ::GetLastError();
124 E : if (error == ERROR_PARTIAL_COPY)
125 E : return TRUE;
126 :
127 : // Nope, it was a real error.
128 i : LOG(ERROR) << "Failed to read process memory: " << ::common::LogWe(error)
129 : << ".";
130 i : return FALSE;
131 E : }
132 :
133 : } // namespace
134 :
135 : AgentLogger::AgentLogger()
136 E : : trace::common::Service(L"Logger"),
137 E : destination_(NULL),
138 E : symbolize_stack_traces_(true) {
139 E : }
140 :
141 E : AgentLogger::~AgentLogger() {
142 E : if (state() != kStopped) {
143 i : ignore_result(Stop());
144 i : ignore_result(Join());
145 : }
146 E : }
147 :
148 E : bool AgentLogger::StartImpl() {
149 E : LOG(INFO) << "Starting the logging service.";
150 :
151 E : if (!InitRpc())
152 i : return false;
153 :
154 E : if (!StartRpc())
155 i : return false;
156 :
157 E : return true;
158 E : }
159 :
160 E : bool AgentLogger::StopImpl() {
161 E : if (!StopRpc())
162 i : return false;
163 E : return true;
164 E : }
165 :
166 E : bool AgentLogger::JoinImpl() {
167 : // Finish processing all RPC events. If Stop() has previously been called
168 : // this will simply ensure that all outstanding requests are handled. If
169 : // Stop has not been called, this will continue (i.e., block) handling events
170 : // until someone else calls Stop() in another thread.
171 E : if (!FinishRpc())
172 i : return false;
173 :
174 E : return true;
175 E : }
176 :
177 : bool AgentLogger::AppendTrace(HANDLE process,
178 : const DWORD* trace_data,
179 : size_t trace_length,
180 E : std::string* message) {
181 E : DCHECK(trace_data != NULL);
182 E : DCHECK(message != NULL);
183 :
184 : // If we don't want to symbolize the stack traces then we just dump the
185 : // frame addresses.
186 E : if (!symbolize_stack_traces_) {
187 i : for (size_t i = 0; i < trace_length; ++i) {
188 i : DWORD frame_ptr = trace_data[i];
189 i : base::StringAppendF(message,
190 : " #%d 0x%012llx\n",
191 : i,
192 : frame_ptr);
193 i : }
194 i : return true;
195 : }
196 :
197 : // TODO(rogerm): Add an RPC session to the logger and its interface. This
198 : // would serialize calls per process and provide a convenient mechanism
199 : // for ensuring SymInitialize/Cleanup are called exactly once per client
200 : // process.
201 :
202 E : base::AutoLock auto_lock(symbol_lock_);
203 :
204 : // Make a unique "handle" for this use of the symbolizer.
205 E : base::win::ScopedHandle unique_handle(
206 : ::OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE,
207 : ::GetCurrentProcessId()));
208 :
209 : // Initializes the symbols for the process:
210 : // - Defer symbol load until they're needed
211 : // - Use undecorated names
212 : // - Get line numbers
213 E : ::SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_UNDNAME | SYMOPT_LOAD_LINES);
214 E : if (!::common::SymInitialize(unique_handle.Get(), NULL, true))
215 i : return false;
216 :
217 : // Try to find the PDB of the running process, if it's found its path will be
218 : // appended to the current symbol search path. It is necessary because the
219 : // default search path doesn't include the directory of the caller by default.
220 : // TODO(sebmarchand): Also append the path of the PDBs of the modules loaded
221 : // by the running process.
222 : WCHAR temp_path[MAX_PATH];
223 E : if (::GetModuleFileNameEx(process, NULL, temp_path, MAX_PATH) != 0) {
224 E : base::FilePath module_path(temp_path);
225 E : base::FilePath temp_pdb_path;
226 E : if (pe::FindPdbForModule(module_path, &temp_pdb_path)) {
227 : char current_search_path[1024];
228 E : if (!::SymGetSearchPath(unique_handle.Get(), current_search_path,
229 : arraysize(current_search_path))) {
230 i : DWORD error = ::GetLastError();
231 i : LOG(ERROR) << "Unable to get the current symbol search path: "
232 : << ::common::LogWe(error);
233 i : return false;
234 : }
235 E : std::string new_pdb_search_path = std::string(current_search_path) + ";" +
236 : temp_pdb_path.DirName().AsUTF8Unsafe();
237 E : if (!::SymSetSearchPath(unique_handle.Get(),
238 : new_pdb_search_path.c_str())) {
239 i : LOG(ERROR) << "Unable to set the symbol search path.";
240 i : return false;
241 : }
242 E : }
243 E : }
244 :
245 : // Append each line of the trace to the message string.
246 E : for (size_t i = 0; i < trace_length; ++i) {
247 E : DWORD frame_ptr = trace_data[i];
248 E : DWORD64 offset = 0;
249 E : std::string symbol_name;
250 E : std::string line_info;
251 :
252 E : GetSymbolInfo(unique_handle.Get(), frame_ptr, &symbol_name, &offset);
253 E : GetLineInfo(unique_handle.Get(), frame_ptr, &line_info);
254 :
255 E : base::StringAppendF(message,
256 : " #%d 0x%012llx in %s%s%s\n",
257 : i,
258 : frame_ptr + offset,
259 : symbol_name.c_str(),
260 : line_info.empty() ? "" : " ",
261 : line_info.c_str());
262 E : }
263 :
264 E : if (!::SymCleanup(unique_handle.Get())) {
265 i : DWORD error = ::GetLastError();
266 i : LOG(ERROR) << "SymCleanup failed: " << ::common::LogWe(error) << ".";
267 i : return false;
268 : }
269 :
270 E : return true;
271 E : }
272 :
273 : bool AgentLogger::CaptureRemoteTrace(HANDLE process,
274 : CONTEXT* context,
275 E : std::vector<DWORD>* trace_data) {
276 E : DCHECK(context != NULL);
277 E : DCHECK(trace_data != NULL);
278 :
279 : // TODO(rogerm): Add an RPC session to the logger and its interface. This
280 : // would serialize calls per process and provide a convenient mechanism
281 : // for ensuring SymInitialize/Cleanup are called exactly once per client
282 : // process.
283 :
284 E : trace_data->clear();
285 E : trace_data->reserve(64);
286 :
287 : // If we don't want to symbolize the stack trace then there's no reason to
288 : // capture it.
289 E : if (!symbolize_stack_traces_) {
290 i : return true;
291 : }
292 :
293 E : base::AutoLock auto_lock(symbol_lock_);
294 :
295 : // Make a unique "handle" for this use of the symbolizer.
296 E : base::win::ScopedHandle unique_handle(
297 : ::OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE,
298 : ::GetCurrentProcessId()));
299 :
300 : // Initializes the symbols for the process:
301 : // - Defer symbol load until they're needed
302 : // - Use undecorated names
303 : // - Get line numbers
304 E : ::SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_UNDNAME | SYMOPT_LOAD_LINES);
305 E : if (!::common::SymInitialize(unique_handle.Get(), NULL, true))
306 i : return false;
307 :
308 : // Initialize a stack frame structure.
309 : STACKFRAME64 stack_frame;
310 E : ::memset(&stack_frame, 0, sizeof(stack_frame));
311 : #if defined(_WIN64)
312 : int machine_type = IMAGE_FILE_MACHINE_AMD64;
313 : stack_frame.AddrPC.Offset = context->Rip;
314 : stack_frame.AddrFrame.Offset = context->Rbp;
315 : stack_frame.AddrStack.Offset = context->Rsp;
316 : #else
317 E : int machine_type = IMAGE_FILE_MACHINE_I386;
318 E : stack_frame.AddrPC.Offset = context->Eip;
319 E : stack_frame.AddrFrame.Offset = context->Ebp;
320 E : stack_frame.AddrStack.Offset = context->Esp;
321 : #endif
322 E : stack_frame.AddrPC.Mode = AddrModeFlat;
323 E : stack_frame.AddrFrame.Mode = AddrModeFlat;
324 E : stack_frame.AddrStack.Mode = AddrModeFlat;
325 :
326 : // Walk the stack.
327 E : while (::StackWalk64(machine_type, unique_handle.Get(), NULL, &stack_frame,
328 : context, &ReadProcessMemoryProc64,
329 : &::SymFunctionTableAccess64, &::SymGetModuleBase64,
330 : NULL)) {
331 E : trace_data->push_back(stack_frame.AddrPC.Offset);
332 E : }
333 :
334 E : if (!::SymCleanup(unique_handle.Get())) {
335 i : DWORD error = ::GetLastError();
336 i : LOG(ERROR) << "SymCleanup failed: " << ::common::LogWe(error) << ".";
337 i : return false;
338 : }
339 :
340 : // And we're done.
341 E : return true;
342 E : }
343 :
344 E : bool AgentLogger::Write(const base::StringPiece& message) {
345 E : DCHECK(destination_ != NULL);
346 :
347 E : if (message.empty())
348 E : return true;
349 :
350 E : base::AutoLock auto_lock(write_lock_);
351 :
352 E : size_t chars_written = ::fwrite(message.data(),
353 : sizeof(std::string::value_type),
354 : message.size(),
355 : destination_);
356 :
357 E : if (chars_written != message.size()) {
358 i : LOG(ERROR) << "Failed to write log message.";
359 i : return false;
360 : }
361 :
362 E : if (message[message.size() - 1] != '\n' &&
363 : ::fwrite("\n", 1, 1, destination_) != 1) {
364 i : LOG(ERROR) << "Failed to append trailing newline.";
365 i : return false;
366 : }
367 :
368 E : ::fflush(destination_);
369 :
370 E : return true;
371 E : }
372 :
373 : bool AgentLogger::SaveMinidumpWithProtobufAndMemoryRanges(
374 : HANDLE process,
375 : base::ProcessId pid,
376 : DWORD tid,
377 : DWORD exc_ptr,
378 : const byte* protobuf,
379 : size_t protobuf_length,
380 : const void* const* memory_ranges_base_addresses,
381 : const size_t* memory_ranges_lengths,
382 E : size_t memory_ranges_count) {
383 E : DCHECK_NE(static_cast<const byte*>(nullptr), protobuf);
384 E : DCHECK_NE(static_cast<const void* const*>(nullptr),
385 E : memory_ranges_base_addresses);
386 E : DCHECK_NE(static_cast<const size_t*>(nullptr), memory_ranges_lengths);
387 :
388 : // Copy the memory ranges into a vector.
389 E : std::vector<kasko::api::MemoryRange> memory_ranges;
390 E : for (size_t i = 0; i < memory_ranges_count; ++i) {
391 E : kasko::api::MemoryRange memory_range = {memory_ranges_base_addresses[i],
392 E : memory_ranges_lengths[i]};
393 E : memory_ranges.push_back(memory_range);
394 E : }
395 :
396 E : kasko::MinidumpRequest request;
397 E : request.client_exception_pointers = true;
398 E : request.exception_info_address = exc_ptr;
399 E : if (protobuf_length) {
400 : kasko::MinidumpRequest::CustomStream custom_stream = {
401 E : kasko::api::kProtobufStreamType, protobuf, protobuf_length};
402 E : request.custom_streams.push_back(custom_stream);
403 : }
404 :
405 E : request.type = kasko::MinidumpRequest::LARGER_DUMP_TYPE;
406 :
407 E : DCHECK(!minidump_dir_.empty());
408 : // Create a temporary file to which to write the minidump. We'll rename it
409 : // to something recognizable when we're finished writing to it.
410 E : base::FilePath temp_file_path;
411 E : if (!base::CreateTemporaryFileInDir(minidump_dir_, &temp_file_path)) {
412 i : LOG(ERROR) << "Could not create mini dump file in "
413 : << minidump_dir_.value();
414 i : return false;
415 : }
416 :
417 : {
418 E : base::AutoLock auto_lock(symbol_lock_);
419 E : base::win::ScopedHandle target_process_handle(::OpenProcess(
420 : GetRequiredAccessForMinidumpType(request.type), FALSE, pid));
421 E : if (!target_process_handle.IsValid()) {
422 i : LOG(ERROR) << "Failed to open target process: " << ::common::LogWe()
423 : << ".";
424 i : return false;
425 : }
426 E : CHECK(kasko::GenerateMinidump(temp_file_path, target_process_handle.Get(),
427 : tid, request));
428 E : }
429 :
430 : // Rename the temporary file so that its recognizable as a dump.
431 E : base::FilePath final_name(
432 : base::StringPrintf(L"minidump-%08u-%08u-%08u.dmp",
433 : pid, tid, ::GetTickCount()));
434 E : base::FilePath final_path = minidump_dir_.Append(final_name);
435 E : if (base::Move(temp_file_path, final_path)) {
436 E : std::string log_msg = base::StringPrintf(
437 : "A minidump has been written to %s.",
438 : final_path.AsUTF8Unsafe().c_str());
439 E : Write(log_msg);
440 E : } else {
441 i : DWORD error = ::GetLastError();
442 i : LOG(ERROR) << "Failed to move dump file to final location "
443 : << ::common::LogWe(error) << ".";
444 i : return false;
445 : }
446 :
447 E : return true;
448 E : }
449 :
450 E : bool AgentLogger::InitRpc() {
451 E : RPC_STATUS status = RPC_S_OK;
452 :
453 : // Initialize the RPC protocol we want to use.
454 E : std::wstring protocol(kLoggerRpcProtocol);
455 E : std::wstring endpoint(
456 : GetInstanceString(kLoggerRpcEndpointRoot, instance_id()));
457 :
458 E : VLOG(1) << "Initializing RPC endpoint '" << endpoint << "' "
459 : << "using the '" << protocol << "' protocol.";
460 E : status = ::RpcServerUseProtseqEp(
461 : ::common::rpc::AsRpcWstr(&protocol[0]), RPC_C_LISTEN_MAX_CALLS_DEFAULT,
462 : ::common::rpc::AsRpcWstr(&endpoint[0]), NULL /* Security descriptor. */);
463 E : if (status != RPC_S_OK && status != RPC_S_DUPLICATE_ENDPOINT) {
464 i : LOG(ERROR) << "Failed to init RPC protocol: " << ::common::LogWe(status)
465 : << ".";
466 i : return false;
467 : }
468 :
469 : // Register the logger interface.
470 E : VLOG(1) << "Registering the Logger interface.";
471 E : status = ::RpcServerRegisterIf(
472 : LoggerService_Logger_v1_0_s_ifspec, NULL, NULL);
473 E : if (status != RPC_S_OK) {
474 i : LOG(ERROR) << "Failed to register RPC interface: "
475 : << ::common::LogWe(status) << ".";
476 i : return false;
477 : }
478 :
479 : // Register the logger control interface.
480 E : VLOG(1) << "Registering the Logger Control interface.";
481 E : status = ::RpcServerRegisterIf(
482 : LoggerService_LoggerControl_v1_0_s_ifspec, NULL, NULL);
483 E : if (status != RPC_S_OK) {
484 i : LOG(ERROR) << "Failed to register RPC interface: "
485 : << ::common::LogWe(status) << ".";
486 i : return false;
487 : }
488 :
489 E : OnInitialized();
490 :
491 E : return true;
492 E : }
493 :
494 E : bool AgentLogger::StartRpc() {
495 : // This method must be called by the owning thread, so no need to otherwise
496 : // synchronize the method invocation.
497 E : VLOG(1) << "Starting the RPC server.";
498 :
499 E : RPC_STATUS status = ::RpcServerListen(
500 : 1, // Minimum number of handler threads.
501 : RPC_C_LISTEN_MAX_CALLS_DEFAULT,
502 : TRUE);
503 :
504 E : if (status != RPC_S_OK) {
505 i : LOG(ERROR) << "Failed to run RPC server: " << ::common::LogWe(status)
506 : << ".";
507 i : ignore_result(FinishRpc());
508 i : return false;
509 : }
510 :
511 : // Signal that the RPC is up and running.
512 E : DCHECK(!started_event_.IsValid());
513 E : std::wstring event_name;
514 E : GetSyzygyAgentLoggerEventName(instance_id(), &event_name);
515 E : started_event_.Set(::CreateEvent(NULL, TRUE, FALSE, event_name.c_str()));
516 E : if (!started_event_.IsValid()) {
517 i : DWORD error = ::GetLastError();
518 i : LOG(ERROR) << "Failed to create event: " << ::common::LogWe(error) << ".";
519 i : return false;
520 : }
521 E : BOOL success = ::SetEvent(started_event_.Get());
522 E : DCHECK(success);
523 :
524 : // Invoke the callback for the logger started event, giving it a chance to
525 : // abort the startup.
526 E : if (!OnStarted()) {
527 i : ignore_result(StopRpc());
528 i : ignore_result(FinishRpc());
529 i : return false;
530 : }
531 :
532 E : return true;
533 E : }
534 :
535 E : bool AgentLogger::StopRpc() {
536 : // This method may be called by any thread, but it does not inspect or modify
537 : // the internal state of the Logger; so, no synchronization is required.
538 E : VLOG(1) << "Requesting an asynchronous shutdown of the logging service.";
539 :
540 E : BOOL success = ::ResetEvent(started_event_.Get());
541 E : DCHECK(success);
542 :
543 E : RPC_STATUS status = ::RpcMgmtStopServerListening(NULL);
544 E : if (status != RPC_S_OK) {
545 i : LOG(ERROR) << "Failed to stop the RPC server: "
546 : << ::common::LogWe(status) << ".";
547 i : return false;
548 : }
549 :
550 E : if (!OnInterrupted())
551 i : return false;
552 :
553 E : return true;
554 E : }
555 :
556 E : bool AgentLogger::FinishRpc() {
557 E : bool error = false;
558 E : RPC_STATUS status = RPC_S_OK;
559 :
560 : // Run the RPC server to completion. This is a blocking call which will only
561 : // terminate after someone calls StopRpc() on another thread.
562 E : status = RpcMgmtWaitServerListen();
563 E : if (status != RPC_S_OK) {
564 i : LOG(ERROR) << "Failed to wait for RPC server shutdown: "
565 : << ::common::LogWe(status) << ".";
566 i : error = true;
567 : }
568 :
569 E : status = ::RpcServerUnregisterIf(
570 : LoggerService_Logger_v1_0_s_ifspec, NULL, FALSE);
571 E : if (status != RPC_S_OK) {
572 i : LOG(ERROR) << "Failed to unregister the AgentLogger RPC interface: "
573 : << ::common::LogWe(status) << ".";
574 i : error = true;
575 : }
576 :
577 E : status = ::RpcServerUnregisterIf(
578 : LoggerService_LoggerControl_v1_0_s_ifspec, NULL, FALSE);
579 E : if (status != RPC_S_OK) {
580 i : LOG(ERROR) << "Failed to unregister AgentLogger Control RPC interface: "
581 : << ::common::LogWe(status) << ".";
582 i : error = true;
583 : }
584 :
585 E : LOG(INFO) << "The logging service has stopped.";
586 E : if (!OnStopped())
587 i : error = true;
588 :
589 E : return !error;
590 E : }
591 :
592 : void AgentLogger::GetSyzygyAgentLoggerEventName(const base::StringPiece16& id,
593 E : std::wstring* output) {
594 E : DCHECK(output != NULL);
595 E : const wchar_t* const kAgentLoggerSvcEvent = L"syzygy-agent-logger-svc-event";
596 :
597 E : output->assign(kAgentLoggerSvcEvent);
598 E : if (!id.empty()) {
599 E : output->append(1, '-');
600 E : output->append(id.begin(), id.end());
601 : }
602 E : }
603 :
604 : } // namespace agent_logger
605 : } // namespace trace
|