]> granicus.if.org Git - icinga2/blob - lib/cli/troubleshootcommand.cpp
add some object locking to the Dump method (which could theoreticylly suffer from...
[icinga2] / lib / cli / troubleshootcommand.cpp
1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2
3 #include "base/application.hpp"
4 #include "base/console.hpp"
5 #include "base/convert.hpp"
6 #include "base/json.hpp"
7 #include "base/netstring.hpp"
8 #include "base/objectlock.hpp"
9 #include "base/stdiostream.hpp"
10 #include "cli/daemonutility.hpp"
11 #include "cli/featureutility.hpp"
12 #include "cli/objectlistutility.hpp"
13 #include "cli/troubleshootcommand.hpp"
14 #include "cli/variableutility.hpp"
15 #include "config/configitembuilder.hpp"
16
17 #include <boost/algorithm/string/join.hpp>
18 #include <boost/circular_buffer.hpp>
19 #include <boost/filesystem.hpp>
20
21 #include <fstream>
22 #include <iostream>
23
24 using namespace icinga;
25 namespace po = boost::program_options;
26
27 REGISTER_CLICOMMAND("troubleshoot", TroubleshootCommand);
28
29 String TroubleshootCommand::GetDescription() const
30 {
31         return "Collect logs and other relevant information for troubleshooting purposes.";
32 }
33
34 String TroubleshootCommand::GetShortDescription() const
35 {
36         return "collect information for troubleshooting";
37 }
38
39 class TroubleshootCommand::InfoLog
40 {
41 public:
42         InfoLog(const String& path, const bool cons)
43         {
44                 m_Console = cons;
45                 m_ConsoleType = Console_Dumb;
46                 if (m_Console) {
47                         m_Stream = new std::ostream(std::cout.rdbuf());
48 #ifndef _WIN32
49                         m_ConsoleType = Console_VT100;
50 #else /*_WIN32*/
51                         m_ConsoleType = Console_Windows;
52 #endif /*_WIN32*/
53                 }
54                 else {
55                         auto *ofs = new std::ofstream();
56                         ofs->open(path.CStr(), std::ios::out | std::ios::trunc);
57                         m_Stream = ofs;
58                 }
59         }
60
61         ~InfoLog()
62         {
63                 delete m_Stream;
64         }
65
66         void WriteLine(const LogSeverity sev, const int color, const String& str)
67         {
68                 if (!m_Console)
69                         Log(sev, "troubleshoot", str);
70
71                 if (sev == LogWarning) {
72                         *m_Stream
73                                 << '\n' << ConsoleColorTag(Console_ForegroundYellow, m_ConsoleType) << std::string(24, '#') << '\n'
74                                 << ConsoleColorTag(Console_Normal, m_ConsoleType) << str
75                                 << ConsoleColorTag(Console_ForegroundYellow, m_ConsoleType) << std::string(24, '#') << "\n\n"
76                                 << ConsoleColorTag(Console_Normal, m_ConsoleType);
77                 } else if (sev == LogCritical) {
78                         *m_Stream
79                                 << '\n' << ConsoleColorTag(Console_ForegroundRed, m_ConsoleType) << std::string(24, '#') << '\n'
80                                 << ConsoleColorTag(Console_Normal, m_ConsoleType) << str
81                                 << ConsoleColorTag(Console_ForegroundRed, m_ConsoleType) << std::string(24, '#') << "\n\n"
82                                 << ConsoleColorTag(Console_Normal, m_ConsoleType);
83                 } else
84                         *m_Stream
85                                 << ConsoleColorTag(color, m_ConsoleType) << str
86                                 << ConsoleColorTag(Console_Normal, m_ConsoleType);
87         }
88
89         bool GetStreamHealth() const
90         {
91                 return m_Stream->good();
92         }
93
94 private:
95         bool m_Console;
96         ConsoleType m_ConsoleType;
97         std::ostream *m_Stream;
98 };
99
100 class TroubleshootCommand::InfoLogLine
101 {
102 public:
103         InfoLogLine(InfoLog& log, int col = Console_Normal, LogSeverity sev = LogInformation)
104                 : m_Log(log), m_Color(col), m_Sev(sev) {}
105
106         ~InfoLogLine()
107         {
108                 m_Log.WriteLine(m_Sev, m_Color, m_String.str());
109         }
110
111         template <typename T>
112         InfoLogLine& operator<<(const T& info)
113         {
114                 m_String << info;
115                 return *this;
116         }
117
118 private:
119         std::ostringstream m_String;
120         InfoLog& m_Log;
121         int m_Color;
122         LogSeverity m_Sev;
123 };
124
125
126 bool TroubleshootCommand::GeneralInfo(InfoLog& log, const boost::program_options::variables_map& vm)
127 {
128         InfoLogLine(log, Console_ForegroundBlue)
129                 << std::string(14, '=') << " GENERAL INFORMATION " << std::string(14, '=') << "\n\n";
130
131         //Application::DisplayInfoMessage() but formatted
132         InfoLogLine(log)
133                 << "\tApplication version: " << Application::GetAppVersion() << '\n'
134                 << "\t\n"
135                 << "\tConfig directory: " << Configuration::ConfigDir << "\n"
136                 << "\tData directory: " << Configuration::DataDir << "\n"
137                 << "\tLog directory: " << Configuration::LogDir << "\n"
138                 << "\tCache directory: " << Configuration::CacheDir << "\n"
139                 << "\tRun directory: " << Configuration::InitRunDir << "\n"
140                 << "\t\n"
141                 << "Old paths (deprecated):\n"
142                 << "\tInstallation root: " << Configuration::PrefixDir << '\n'
143                 << "\tSysconf directory: " << Configuration::SysconfDir << '\n'
144                 << "\tRun directory: " << Configuration::RunDir << '\n'
145                 << "\tLocal state directory: " << Configuration::LocalStateDir << '\n'
146                 << "\t\n"
147                 << "Internal paths:\n"
148                 << "\tPackage data directory: " << Configuration::PkgDataDir << '\n'
149                 << "\tState path: " << Configuration::StatePath << '\n'
150                 << "\tObjects path: " << Configuration::ObjectsPath << '\n'
151                 << "\tVars path: " << Configuration::VarsPath << '\n'
152                 << "\tPID path: " << Configuration::PidPath << '\n';
153
154         InfoLogLine(log)
155                 << '\n';
156
157         return true;
158 }
159
160 bool TroubleshootCommand::FeatureInfo(InfoLog& log, const boost::program_options::variables_map& vm)
161 {
162         TroubleshootCommand::CheckFeatures(log);
163         //TODO Check whether active features are operational.
164         return true;
165 }
166
167 bool TroubleshootCommand::ObjectInfo(InfoLog& log, const boost::program_options::variables_map& vm, Dictionary::Ptr& logs, const String& path)
168 {
169         InfoLogLine(log, Console_ForegroundBlue)
170                 << std::string(14, '=') << " OBJECT INFORMATION " << std::string(14, '=') << "\n\n";
171
172         String objectfile = Configuration::ObjectsPath;
173         std::set<String> configs;
174
175         if (!Utility::PathExists(objectfile)) {
176                 InfoLogLine(log, 0, LogCritical)
177                         << "Cannot open object file '" << objectfile << "'.\n"
178                         << "FAILED: This probably means you have a fault configuration.\n";
179                 return false;
180         } else {
181                 InfoLog *OFile = nullptr;
182                 bool OConsole = false;
183                 if (vm.count("include-objects")) {
184                         if (vm.count("console"))
185                                 OConsole = true;
186                         else {
187                                 OFile = new InfoLog(path+"-objects", false);
188                                 if (!OFile->GetStreamHealth()) {
189                                         InfoLogLine(log, 0, LogWarning)
190                                                 << "Failed to open Object-write-stream, not printing objects\n\n";
191                                         delete OFile;
192                                         OFile = nullptr;
193                                 } else
194                                         InfoLogLine(log)
195                                                 << "Printing all objects to " << path+"-objects\n";
196                         }
197                 }
198                 CheckObjectFile(objectfile, log, OFile, OConsole, logs, configs);
199                 delete OFile;
200         }
201
202         if (vm.count("include-vars")) {
203                 if (vm.count("console")) {
204                         InfoLogLine(log, Console_ForegroundBlue)
205                                 << "\n[begin: varsfile]\n";
206                         if (!PrintVarsFile(path, true))
207                                 InfoLogLine(log, 0, LogWarning)
208                                         << "Failed to print vars file\n";
209                         InfoLogLine(log, Console_ForegroundBlue)
210                                 << "[end: varsfile]\n";
211                 } else {
212                         if (PrintVarsFile(path, false))
213                                 InfoLogLine(log)
214                                         << "Successfully printed all variables to " << path+"-vars\n";
215                         else
216                                 InfoLogLine(log, 0, LogWarning)
217                                         << "Failed to print vars to " << path+"-vars\n";
218                 }
219         }
220
221         InfoLogLine(log)
222                 << '\n';
223
224         return true;
225 }
226
227 bool TroubleshootCommand::ReportInfo(InfoLog& log, const boost::program_options::variables_map& vm, Dictionary::Ptr& logs)
228 {
229         InfoLogLine(log, Console_ForegroundBlue)
230                 << std::string(14, '=') << " LOGS AND CRASH REPORTS " << std::string(14, '=') << "\n\n";
231         PrintLoggers(log, logs);
232         PrintCrashReports(log);
233
234         InfoLogLine(log)
235                 << '\n';
236
237         return true;
238 }
239
240 bool TroubleshootCommand::ConfigInfo(InfoLog& log, const boost::program_options::variables_map& vm)
241 {
242         InfoLogLine(log, Console_ForegroundBlue)
243                 << std::string(14, '=') << " CONFIGURATION FILES " << std::string(14, '=') << "\n\n";
244
245         InfoLogLine(log)
246                 << "A collection of important configuration files follows, please make sure to remove any sensitive data such as credentials, internal company names, etc\n";
247
248         if (!PrintFile(log, Configuration::ConfigDir + "/icinga2.conf")) {
249                 InfoLogLine(log, 0, LogWarning)
250                         << "icinga2.conf not found, therefore skipping validation.\n"
251                         << "If you are using an icinga2.conf somewhere but the default path please validate it via 'icinga2 daemon -C -c \"path\to/icinga2.conf\"'\n"
252                         << "and provide it with your support request.\n";
253         }
254
255         if (!PrintFile(log, Configuration::ConfigDir + "/zones.conf")) {
256                 InfoLogLine(log, 0, LogWarning)
257                         << "zones.conf not found.\n"
258                         << "If you are using a zones.conf somewhere but the default path please provide it with your support request\n";
259         }
260
261         InfoLogLine(log)
262                 << '\n';
263
264         return true;
265 }
266
267 /*Print the last *numLines* of *file* to *os* */
268 int TroubleshootCommand::Tail(const String& file, int numLines, InfoLog& log)
269 {
270         boost::circular_buffer<std::string> ringBuf(numLines);
271         std::ifstream text;
272         text.open(file.CStr(), std::ifstream::in);
273         if (!text.good())
274                 return 0;
275
276         std::string line;
277         int lines = 0;
278
279         while (std::getline(text, line)) {
280                 ringBuf.push_back(line);
281                 lines++;
282         }
283
284         if (lines < numLines)
285                 numLines = lines;
286
287         InfoLogLine(log, Console_ForegroundCyan)
288                 << "[begin: '" << file << "' line: " << lines-numLines << "]\n";
289
290         for (int k = 0; k < numLines; k++) {
291                 InfoLogLine(log, Console_ForegroundCyan)
292                         <<  "#  ";
293                 InfoLogLine(log)
294                         << ringBuf[k] << '\n';
295         }
296
297         text.close();
298
299         InfoLogLine(log, Console_ForegroundCyan)
300                 << "[end: '" << file << "' line: " << lines << "]\n\n";
301
302         return numLines;
303 }
304
305 bool TroubleshootCommand::CheckFeatures(InfoLog& log)
306 {
307         Dictionary::Ptr features = new Dictionary;
308         std::vector<String> disabled_features;
309         std::vector<String> enabled_features;
310
311         if (!FeatureUtility::GetFeatures(disabled_features, true) ||
312                 !FeatureUtility::GetFeatures(enabled_features, false)) {
313                 InfoLogLine(log, 0, LogCritical)
314                         << "Failed to collect enabled and/or disabled features. Check\n"
315                         << FeatureUtility::GetFeaturesAvailablePath() << '\n'
316                         << FeatureUtility::GetFeaturesEnabledPath() << '\n';
317                 return false;
318         }
319
320         for (const String& feature : disabled_features)
321                 features->Set(feature, false);
322         for (const String& feature : enabled_features)
323                 features->Set(feature, true);
324
325         InfoLogLine(log)
326                 << "Enabled features:\n";
327         InfoLogLine(log, Console_ForegroundGreen)
328                 << '\t' << boost::algorithm::join(enabled_features, " ") << '\n';
329         InfoLogLine(log)
330                 << "Disabled features:\n";
331         InfoLogLine(log, Console_ForegroundRed)
332                 << '\t' << boost::algorithm::join(disabled_features, " ") << '\n';
333
334         if (!features->Get("checker").ToBool())
335                 InfoLogLine(log, 0, LogWarning)
336                         << "checker is disabled, no checks can be run from this instance\n";
337         if (!features->Get("mainlog").ToBool())
338                 InfoLogLine(log, 0, LogWarning)
339                         << "mainlog is disabled, please activate it and rerun icinga2\n";
340         if (!features->Get("debuglog").ToBool())
341                 InfoLogLine(log, 0, LogWarning)
342                         << "debuglog is disabled, please activate it and rerun icinga2\n";
343
344         return true;
345 }
346
347 void TroubleshootCommand::GetLatestReport(const String& filename, time_t& bestTimestamp, String& bestFilename)
348 {
349 #ifdef _WIN32
350         struct _stat buf;
351         if (_stat(filename.CStr(), &buf))
352                 return;
353 #else
354         struct stat buf;
355         if (stat(filename.CStr(), &buf))
356                 return;
357 #endif /*_WIN32*/
358         if (buf.st_mtime > bestTimestamp) {
359                 bestTimestamp = buf.st_mtime;
360                 bestFilename = filename;
361         }
362 }
363
364 bool TroubleshootCommand::PrintCrashReports(InfoLog& log)
365 {
366         String spath = Configuration::LogDir + "/crash/report.*";
367         time_t bestTimestamp = 0;
368         String bestFilename;
369
370         try {
371                 Utility::Glob(spath, std::bind(&GetLatestReport, _1, std::ref(bestTimestamp),
372                         std::ref(bestFilename)), GlobFile);
373         }
374 #ifdef _WIN32
375         catch (win32_error &ex) {
376                 if (int const * err = boost::get_error_info<errinfo_win32_error>(ex)) {
377                         if (*err != 3) {//Error code for path does not exist
378                                 InfoLogLine(log, 0, LogWarning)
379                                         << Configuration::LogDir + "/crash/ does not exist\n";
380
381                                 return false;
382                         }
383                 }
384                 InfoLogLine(log, 0, LogWarning)
385                         << "Error printing crash reports\n";
386
387                 return false;
388         }
389 #else
390         catch (...) {
391                 InfoLogLine(log, 0, LogWarning) << "Error printing crash reports.\n"
392                         << "Does " << Configuration::LogDir + "/crash/ exist?\n";
393
394                 return false;
395         }
396 #endif /*_WIN32*/
397
398         if (!bestTimestamp)
399                 InfoLogLine(log, Console_ForegroundYellow)
400                         << "No crash logs found in " << Configuration::LogDir << "/crash/\n\n";
401         else {
402                 InfoLogLine(log)
403                         << "Latest crash report is from " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", Utility::GetTime()) << '\n'
404                         << "File: " << bestFilename << "\n\n";
405                 PrintFile(log, bestFilename);
406                 InfoLogLine(log)
407                         << '\n';
408         }
409
410         return true;
411 }
412
413 bool TroubleshootCommand::PrintFile(InfoLog& log, const String& path)
414 {
415         std::ifstream text;
416         text.open(path.CStr(), std::ifstream::in);
417         if (!text.is_open())
418                 return false;
419
420         std::string line;
421
422         InfoLogLine(log, Console_ForegroundCyan)
423                 << "[begin: '" << path << "']\n";
424
425         while (std::getline(text, line)) {
426                 InfoLogLine(log, Console_ForegroundCyan)
427                         << "#  ";
428                 InfoLogLine(log)
429                         << line << '\n';
430         }
431
432         InfoLogLine(log, Console_ForegroundCyan)
433                 << "[end: '" << path << "']\n";
434
435         return true;
436 }
437
438 bool TroubleshootCommand::CheckConfig()
439 {
440         String configDir = Configuration::ConfigDir;
441         String objectsPath = Configuration::ObjectsPath;
442         return DaemonUtility::ValidateConfigFiles({ configDir + "/icinga2.conf" }, objectsPath);
443 }
444
445 //print is supposed allow the user to print the object file
446 void TroubleshootCommand::CheckObjectFile(const String& objectfile, InfoLog& log, InfoLog *OFile, const bool objectConsole,
447         Dictionary::Ptr& logs, std::set<String>& configs)
448 {
449         InfoLogLine(log)
450                 << "Checking object file from " << objectfile << '\n';
451
452         std::fstream fp;
453         fp.open(objectfile.CStr(), std::ios_base::in);
454
455         if (!fp.is_open()) {
456                 InfoLogLine(log, 0, LogWarning)
457                         << "Could not open object file.\n";
458                 return;
459         }
460
461         StdioStream::Ptr sfp = new StdioStream(&fp, false);
462         String::SizeType typeL = 0, countTotal = 0;
463
464         String message;
465         StreamReadContext src;
466         StreamReadStatus srs;
467         std::map<String, int> type_count;
468         bool first = true;
469
470         std::stringstream sStream;
471
472         if (objectConsole)
473                 InfoLogLine(log, Console_ForegroundBlue)
474                         << "\n[begin: objectfile]\n";
475
476         while ((srs = NetString::ReadStringFromStream(sfp, &message, src)) != StatusEof) {
477                 if (srs != StatusNewItem)
478                         continue;
479
480                 if (objectConsole) {
481                         ObjectListUtility::PrintObject(std::cout, first, message, type_count, "", "");
482                 }
483                 else {
484                 ObjectListUtility::PrintObject(sStream, first, message, type_count, "", "");
485                         if (OFile) {
486                                 InfoLogLine(*OFile)
487                                         << sStream.str();
488                                 sStream.flush();
489                         }
490                 }
491
492                 Dictionary::Ptr object = JsonDecode(message);
493                 Dictionary::Ptr properties = object->Get("properties");
494
495                 String name = object->Get("name");
496                 String type = object->Get("type");
497
498                 //Find longest typename for padding
499                 typeL = type.GetLength() > typeL ? type.GetLength() : typeL;
500                 countTotal++;
501
502                 Array::Ptr debug_info = object->Get("debug_info");
503
504                 if (debug_info)
505                         configs.insert(debug_info->Get(0));
506
507                 if (Utility::Match(type, "FileLogger")) {
508                         Dictionary::Ptr debug_hints = object->Get("debug_hints");
509                         Dictionary::Ptr properties = object->Get("properties");
510
511                         ObjectLock olock(properties);
512                         for (const Dictionary::Pair& kv : properties) {
513                                 if (Utility::Match(kv.first, "path"))
514                                         logs->Set(name, kv.second);
515                         }
516                 }
517         }
518
519         if (objectConsole)
520                 InfoLogLine(log, Console_ForegroundBlue)
521                         << "\n[end: objectfile]\n";
522
523         if (!countTotal) {
524                 InfoLogLine(log, 0, LogCritical)
525                         << "No objects found in objectfile.\n";
526                 return;
527         }
528
529         //Print objects with count
530         InfoLogLine(log)
531                 << "Found the " << countTotal << " objects:\n"
532                 << "  Type" << std::string(typeL-4, ' ') << " : Count\n";
533
534         for (const Dictionary::Pair& kv : type_count) {
535                 InfoLogLine(log)
536                         << "  " << kv.first << std::string(typeL - kv.first.GetLength(), ' ')
537                         << " : " << kv.second << '\n';
538         }
539
540         InfoLogLine(log)
541                 << '\n';
542
543         TroubleshootCommand::PrintObjectOrigin(log, configs);
544 }
545
546 bool TroubleshootCommand::PrintVarsFile(const String& path, const bool console) {
547         if (!console) {
548                 auto *ofs = new std::ofstream();
549                 ofs->open((path+"-vars").CStr(), std::ios::out | std::ios::trunc);
550                 if (!ofs->is_open())
551                         return false;
552                 else
553                         VariableUtility::PrintVariables(*ofs);
554                 ofs->close();
555         } else
556                 VariableUtility::PrintVariables(std::cout);
557
558         return true;
559 }
560
561 void TroubleshootCommand::PrintLoggers(InfoLog& log, Dictionary::Ptr& logs)
562 {
563         if (!logs->GetLength()) {
564                 InfoLogLine(log, 0, LogWarning)
565                         << "No loggers found, check whether you enabled any logging features\n";
566         } else {
567                 InfoLogLine(log)
568                         << "Getting the last 20 lines of " << logs->GetLength() << " FileLogger objects.\n";
569
570                 ObjectLock ulock(logs);
571                 for (const Dictionary::Pair& kv : logs) {
572                         InfoLogLine(log)
573                                 << "Logger " << kv.first << " at path: " << kv.second << '\n';
574
575                         if (!Tail(kv.second, 20, log)) {
576                                 InfoLogLine(log, 0, LogWarning)
577                                         << kv.second << " either does not exist or is empty\n";
578                         }
579                 }
580         }
581 }
582
583 void TroubleshootCommand::PrintObjectOrigin(InfoLog& log, const std::set<String>& configSet)
584 {
585         InfoLogLine(log)
586                 << "The objects origins are:\n";
587
588         for (const String& config : configSet) {
589                 InfoLogLine(log)
590                         << "  " << config << '\n';
591         }
592 }
593
594 void TroubleshootCommand::InitParameters(boost::program_options::options_description& visibleDesc,
595         boost::program_options::options_description& hiddenDesc) const
596 {
597         visibleDesc.add_options()
598                 ("console,c", "print to console instead of file")
599                 ("output,o", boost::program_options::value<std::string>(), "path to output file")
600                 ("include-objects", "Print the whole objectfile (like `object list`)")
601                 ("include-vars", "Print all Variables (like `variable list`)")
602                 ;
603 }
604
605 int TroubleshootCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
606 {
607 #ifdef _WIN32 //Dislikes ':' in filenames
608         String path = Configuration::LogDir + "/troubleshooting-"
609                 + Utility::FormatDateTime("%Y-%m-%d_%H-%M-%S", Utility::GetTime()) + ".log";
610 #else
611         String path = Configuration::LogDir + "/troubleshooting-"
612                 + Utility::FormatDateTime("%Y-%m-%d_%H:%M:%S", Utility::GetTime()) + ".log";
613 #endif /*_WIN32*/
614
615         InfoLog *log;
616         Logger::SetConsoleLogSeverity(LogWarning);
617
618         if (vm.count("output"))
619                 path = vm["output"].as<std::string>();
620
621         if (vm.count("console")) {
622                 log = new InfoLog("", true);
623         } else {
624                 log = new InfoLog(path, false);
625                 if (!log->GetStreamHealth()) {
626                         Log(LogCritical, "troubleshoot", "Failed to open file to write: " + path);
627                         delete log;
628                         return 3;
629                 }
630         }
631
632         String appName = Utility::BaseName(Application::GetArgV()[0]);
633         double goTime = Utility::GetTime();
634
635         InfoLogLine(*log)
636                 << appName << " -- Troubleshooting help:\n"
637                 << "Should you run into problems with Icinga please add this file to your help request\n"
638                 << "Started collection at " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", goTime) << "\n";
639
640         InfoLogLine(*log, Console_ForegroundMagenta)
641                 << std::string(52, '=') << "\n\n";
642
643         if (appName.GetLength() > 3 && appName.SubStr(0, 3) == "lt-")
644                 appName = appName.SubStr(3, appName.GetLength() - 3);
645
646         Dictionary::Ptr logs = new Dictionary;
647
648         if (!GeneralInfo(*log, vm) ||
649                 !FeatureInfo(*log, vm) ||
650                 !ObjectInfo(*log, vm, logs, path) ||
651                 !ReportInfo(*log, vm, logs) ||
652                 !ConfigInfo(*log, vm)) {
653                 InfoLogLine(*log, 0, LogCritical)
654                         << "Could not recover from critical failure, exiting.\n";
655
656                 delete log;
657                 return 3;
658         }
659
660         double endTime = Utility::GetTime();
661
662         InfoLogLine(*log, Console_ForegroundMagenta)
663                 << std::string(52, '=') << '\n';
664         InfoLogLine(*log, Console_ForegroundGreen)
665                 << "Finished collection at " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", endTime)
666                 << "\nTook " << Convert::ToString(endTime - goTime) << " seconds\n";
667
668         if (!vm.count("console")) {
669                 std::cout << "Started collection at " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", goTime) << "\n"
670                         << "Finished collection at " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", endTime)
671                         << "\nTook " << Convert::ToString(endTime - goTime) << " seconds\n\n";
672
673                 std::cout << "General log file: '" << path << "'\n";
674
675                 if (vm.count("include-vars"))
676                         std::cout << "Vars log file: '" << path << "-vars'\n";
677                 if (vm.count("include-objects"))
678                         std::cout << "Objects log file: '" << path << "-objects'\n";
679
680                 std::cout << "\nPlease compress the files before uploading them,, for example:\n"
681                         << "  # tar czf troubleshoot.tar.gz " << path << "*\n";
682         }
683
684         delete log;
685         return 0;
686 }