#include "mon_lwtrace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace NMonitoring; #define WWW_CHECK(cond, ...) \ do { \ if (!(cond)) { \ ythrow yexception() << Sprintf(__VA_ARGS__); \ } \ } while (false) \ /**/ #define WWW_HTML_INNER(out) HTML(out) WITH_SCOPED(tmp, TScopedHtmlInner(out)) #define WWW_HTML(out) out << NMonitoring::HTTPOKHTML; WWW_HTML_INNER(out) namespace NLwTraceMonPage { struct TTrackLogRefs { struct TItem { const TThread::TId ThreadId; const NLWTrace::TLogItem* Ptr; TItem() : ThreadId(0) , Ptr(nullptr) {} TItem(TThread::TId tid, const NLWTrace::TLogItem& ref) : ThreadId(tid) , Ptr(&ref) {} TItem(const TItem& o) : ThreadId(o.ThreadId) , Ptr(o.Ptr) {} operator const NLWTrace::TLogItem&() const { return *Ptr; } }; using TItems = TVector; TItems Items; TTrackLogRefs() {} TTrackLogRefs(const TTrackLogRefs& other) : Items(other.Items) {} void Clear() { Items.clear(); } ui64 GetTimestampCycles() const { return Items.empty()? 0: Items.front().Ptr->GetTimestampCycles(); } }; // // Templates to treat both NLWTrace::TLogItem and NLWTrace::TTrackLog in the same way (e.g. in TLogQuery) // template struct TLogTraits {}; template <> struct TLogTraits { using TLog = NLWTrace::TLogItem; using const_iterator = const NLWTrace::TLogItem*; using const_reverse_iterator = const NLWTrace::TLogItem*; static const_iterator begin(const TLog& log) { return &log; } static const_iterator end(const TLog& log) { return &log + 1; } static const_reverse_iterator rbegin(const TLog& log) { return &log; } static const_reverse_iterator rend(const TLog& log) { return &log + 1; } static bool empty(const TLog&) { return false; } static const NLWTrace::TLogItem& front(const TLog& log) { return log; } static const NLWTrace::TLogItem& back(const TLog& log) { return log; } }; template <> struct TLogTraits { using TLog = NLWTrace::TTrackLog; using const_iterator = NLWTrace::TTrackLog::TItems::const_iterator; using const_reverse_iterator = NLWTrace::TTrackLog::TItems::const_reverse_iterator; static const_iterator begin(const TLog& log) { return log.Items.begin(); } static const_iterator end(const TLog& log) { return log.Items.end(); } static const_reverse_iterator rbegin(const TLog& log) { return log.Items.rbegin(); } static const_reverse_iterator rend(const TLog& log) { return log.Items.rend(); } static bool empty(const TLog& log) { return log.Items.empty(); } static const NLWTrace::TLogItem& front(const TLog& log) { return log.Items.front(); } static const NLWTrace::TLogItem& back(const TLog& log) { return log.Items.back(); } }; template <> struct TLogTraits { using TLog = TTrackLogRefs; using const_iterator = TTrackLogRefs::TItems::const_iterator; using const_reverse_iterator = TTrackLogRefs::TItems::const_reverse_iterator; static const_iterator begin(const TLog& log) { return log.Items.begin(); } static const_iterator end(const TLog& log) { return log.Items.end(); } static const_reverse_iterator rbegin(const TLog& log) { return log.Items.rbegin(); } static const_reverse_iterator rend(const TLog& log) { return log.Items.rend(); } static bool empty(const TLog& log) { return log.Items.empty(); } static const NLWTrace::TLogItem& front(const TLog& log) { return log.Items.front(); } static const NLWTrace::TLogItem& back(const TLog& log) { return log.Items.back(); } }; /* * Log Query Language: * Data expressions: * 1) myparam[0], myparam[-1] // the first and the last myparam in any probe/provider * 2) myparam // the first (the same as [0]) * 3) PROVIDER..myparam // any probe with myparam in PROVIDER * 4) MyProbe._elapsedMs // Ms since track begin for the first MyProbe event * 5) PROVIDER.MyProbe._sliceUs // Us since previous event in track for the first PROVIDER.MyProbe event */ struct TLogQuery { private: enum ESpecialParam { NotSpecial = 0, // The last '*' can be one of: Ms, Us, Ns, Min, Hours, (blank means seconds) // '*Time' can be one of: RTime (since cut ts for given dataset), NTime (negative time since now), Time (since machine start) TrackDuration = 1, // _track* TrackBeginTime = 2, // _begin*Time* TrackEndTime = 3, // _end*Time* ElapsedDuration = 4, // _elapsed* SliceDuration = 5, // _slice* ThreadTime = 6, // _thr*Time* }; template > struct TExecuteQuery; template friend struct TExecuteQuery; template struct TExecuteQuery { const TLogQuery& Query; const TLog* Log = nullptr; bool Reversed; i64 Skip; const NLWTrace::TLogItem* Item = nullptr; typename TTr::const_iterator FwdIter; typename TTr::const_reverse_iterator RevIter; NLWTrace::TTypedParam Result; explicit TExecuteQuery(const TLogQuery& query, const TLog& log) : Query(query) , Log(&log) , Reversed(Query.Index < 0) , Skip(Reversed? -Query.Index - 1: Query.Index) , FwdIter() , RevIter() {} void ExecuteQuery() { if (!Reversed) { for (auto i = TTr::begin(*Log), e = TTr::end(*Log); i != e; ++i) { if (FwdIteration(i)) { return; } } } else { for (auto i = TTr::rbegin(*Log), e = TTr::rend(*Log); i != e; ++i) { if (RevIteration(i)) { return; } } } } bool FwdIteration(typename TTr::const_iterator it) { FwdIter = it; Item = &*it; return ProcessItem(); } bool RevIteration(typename TTr::const_reverse_iterator it) { RevIter = it; Item = &*it; return ProcessItem(); } bool ProcessItem() { if (Query.Provider && Query.Provider != Item->Probe->Event.GetProvider()) { return false; } if (Query.Probe && Query.Probe != Item->Probe->Event.Name) { return false; } switch (Query.SpecialParam) { case NotSpecial: if (Item->Probe->Event.Signature.FindParamIndex(Query.ParamName) != size_t(-1)) { break; // param found } else { return false; // param not found } case TrackDuration: Y_ABORT(); case TrackBeginTime: Y_ABORT(); case TrackEndTime: Y_ABORT(); case ElapsedDuration: break; case SliceDuration: break; case ThreadTime: break; } if (Skip > 0) { Skip--; return false; } switch (Query.SpecialParam) { case NotSpecial: Result = NLWTrace::TTypedParam(Item->GetParam(Query.ParamName)); return true; case TrackDuration: Y_ABORT(); case TrackBeginTime: Y_ABORT(); case TrackEndTime: Y_ABORT(); case ElapsedDuration: Result = NLWTrace::TTypedParam(Query.Duration( Log->GetTimestampCycles(), Item->GetTimestampCycles())); return true; case SliceDuration: Result = NLWTrace::TTypedParam(Query.Duration( PrevOrSame().GetTimestampCycles(), Item->GetTimestampCycles())); return true; case ThreadTime: Result = NLWTrace::TTypedParam(Query.Instant(Item->GetTimestampCycles())); return true; } return true; } const NLWTrace::TLogItem& PrevOrSame() const { if (!Reversed) { auto i = FwdIter; if (i != TTr::begin(*Log)) { i--; } return *i; } else { auto j = RevIter + 1; if (j == TTr::rend(*Log)) { return *RevIter; } return *j; } } }; TString Text; TString Provider; TString Probe; TString ParamName; ESpecialParam SpecialParam = NotSpecial; i64 Index = 0; double TimeUnitSec = 1.0; i64 ZeroTs = 0; i64 RTimeZeroTs = 0; i64 NTimeZeroTs = 0; public: TLogQuery() {} explicit TLogQuery(const TString& text) : Text(text) { try { if (!Text.empty()) { ParseQuery(Text); } } catch (...) { ythrow yexception() << CurrentExceptionMessage() << " while parsing track log query: " << Text; } } operator bool() const { return !Text.empty(); } template NLWTrace::TTypedParam ExecuteQuery(const TLog& log) const { using TTr = TLogTraits; WWW_CHECK(Text, "execute of empty log query"); if (TTr::empty(log)) { return NLWTrace::TTypedParam(); } if (SpecialParam == TrackDuration) { return Duration( log.GetTimestampCycles(), TTr::back(log).GetTimestampCycles()); } else if (SpecialParam == TrackBeginTime) { return Instant(log.GetTimestampCycles()); } else if (SpecialParam == TrackEndTime) { return Instant(TTr::back(log).GetTimestampCycles()); } TExecuteQuery exec(*this, log); exec.ExecuteQuery(); return exec.Result; } private: NLWTrace::TTypedParam Duration(ui64 ts1, ui64 ts2) const { double sec = NHPTimer::GetSeconds(ts1 < ts2? ts2 - ts1: 0); return NLWTrace::TTypedParam(sec / TimeUnitSec); } NLWTrace::TTypedParam Instant(ui64 ts) const { double sec = NHPTimer::GetSeconds(i64(ts) - ZeroTs); return NLWTrace::TTypedParam(sec / TimeUnitSec); } void ParseQuery(const TString& s) { auto parts = SplitString(s, "."); WWW_CHECK(parts.size() <= 3, "too many name specifiers"); ParseParamSelector(parts.back()); if (parts.size() >= 2) { ParseProbeSelector(parts[parts.size() - 2]); } if (parts.size() >= 3) { ParseProviderSelector(parts[parts.size() - 3]); } } void ParseParamSelector(const TString& s) { size_t bracket = s.find('['); if (bracket == TString::npos) { ParseParamName(s); Index = 0; } else { ParseParamName(s.substr(0, bracket)); size_t bracket2 = s.find(']', bracket); WWW_CHECK(bracket2 != TString::npos, "closing braket ']' is missing"); Index = FromString(s.substr(bracket + 1, bracket2 - bracket - 1)); } } void ParseParamName(const TString& s) { ParamName = s; TString paramName = s; const static TVector> specials = { { "_track", TrackDuration }, { "_begin", TrackBeginTime }, { "_end", TrackEndTime }, { "_elapsed", ElapsedDuration }, { "_slice", SliceDuration }, { "_thr", ThreadTime } }; // Check for special params SpecialParam = NotSpecial; for (const auto& p : specials) { if (paramName.StartsWith(p.first)) { SpecialParam = p.second; paramName.erase(0, p.first.size()); break; } } if (SpecialParam == NotSpecial) { return; } const static TVector> timeUnits = { { "Ms", 1e-3 }, { "Us", 1e-6 }, { "Ns", 1e-9 }, { "Min", 60.0 }, { "Hours", 3600.0 } }; // Parse units for special params TimeUnitSec = 1.0; for (const auto& p : timeUnits) { if (paramName.EndsWith(p.first)) { TimeUnitSec = p.second; paramName.erase(paramName.size() - p.first.size()); break; } } if (SpecialParam == ThreadTime || SpecialParam == TrackBeginTime || SpecialParam == TrackEndTime) { // Parse time zero for special instant params const TVector> timeZeros = { { "RTime", RTimeZeroTs }, { "NTime", NTimeZeroTs }, { "Time", 0ll } }; ZeroTs = -1; for (const auto& p : timeZeros) { if (paramName.EndsWith(p.first)) { ZeroTs = p.second; paramName.erase(paramName.size() - p.first.size()); break; } } WWW_CHECK(ZeroTs != -1, "wrong special param name (postfix '*Time' required): %s", s.data()); } WWW_CHECK(paramName.empty(), "wrong special param name: %s", s.data()); } void ParseProbeSelector(const TString& s) { Probe = s; } void ParseProviderSelector(const TString& s) { Provider = s; } }; using TVariants = TVector>; using TTags = TSet; TString GetProbeName(const NLWTrace::TProbe* probe, const char* sep = ".") { return TString(probe->Event.GetProvider()) + sep + probe->Event.Name; } struct TAdHocTraceConfig { NLWTrace::TQuery Cfg; TAdHocTraceConfig() {} // Invalid config TAdHocTraceConfig(ui16 logSize, ui64 logDurationUs, bool logShuttle) { auto block = Cfg.AddBlocks(); // Create one block to distinguish valid config if (logSize) { Cfg.SetPerThreadLogSize(logSize); } if (logDurationUs) { Cfg.SetLogDurationUs(logDurationUs); } if (logShuttle) { block->AddAction()->MutableRunLogShuttleAction(); } } TAdHocTraceConfig(const TString& provider, const TString& probe, ui16 logSize = 0, ui64 logDurationUs = 0, bool logShuttle = false) : TAdHocTraceConfig(logSize, logDurationUs, logShuttle) { auto block = Cfg.MutableBlocks(0); auto pdesc = block->MutableProbeDesc(); pdesc->SetProvider(provider); pdesc->SetName(probe); } explicit TAdHocTraceConfig(const TString& group, ui16 logSize = 0, ui64 logDurationUs = 0, bool logShuttle = false) : TAdHocTraceConfig(logSize, logDurationUs, logShuttle) { auto block = Cfg.MutableBlocks(0); auto pdesc = block->MutableProbeDesc(); pdesc->SetGroup(group); } TString Id() const { TStringStream ss; for (size_t blockIdx = 0; blockIdx < Cfg.BlocksSize(); blockIdx++) { if (!ss.Str().empty()) { ss << "/"; } auto block = Cfg.GetBlocks(blockIdx); auto pdesc = block.GetProbeDesc(); if (pdesc.GetProvider()) { ss << "." << pdesc.GetProvider() << "." << pdesc.GetName(); } else if (pdesc.GetGroup()) { ss << ".Group." << pdesc.GetGroup(); } // TODO[serxa]: handle predicate for (size_t actionIdx = 0; actionIdx < block.ActionSize(); actionIdx++) { const NLWTrace::TAction& action = block.GetAction(actionIdx); if (action.HasRunLogShuttleAction()) { auto ls = action.GetRunLogShuttleAction(); ss << ".alsr"; if (ls.GetIgnore()) { ss << "-i"; } if (ls.GetShuttlesCount()) { ss << "-s" << ls.GetShuttlesCount(); } if (ls.GetMaxTrackLength()) { ss << "-t" << ls.GetMaxTrackLength(); } } else if (action.HasEditLogShuttleAction()) { auto ls = action.GetEditLogShuttleAction(); ss << ".alse"; if (ls.GetIgnore()) { ss << "-i"; } } else if (action.HasDropLogShuttleAction()) { ss << ".alsd"; } } } if (Cfg.GetPerThreadLogSize()) { ss << ".l" << Cfg.GetPerThreadLogSize(); } if (Cfg.GetLogDurationUs()) { ui64 logDurationUs = Cfg.GetLogDurationUs(); if (logDurationUs % (60 * 1000 * 1000) == 0) ss << ".d" << logDurationUs / (60 * 1000 * 1000) << "m"; if (logDurationUs % (1000 * 1000) == 0) ss << ".d" << logDurationUs / (1000 * 1000) << "s"; else if (logDurationUs % 1000 == 0) ss << ".d" << logDurationUs / 1000 << "ms"; else ss << ".d" << logDurationUs << "us"; } return ss.Str(); } bool IsValid() const { return Cfg.BlocksSize() > 0; } NLWTrace::TQuery Query() const { if (!IsValid()) { ythrow yexception() << "invalid adhoc trace config"; } return Cfg; } bool ParseId(const TString& id) { if (IsAdHocId(id)) { for (const TString& block : SplitString(id, "/")) { if (block.empty()) { continue; } size_t cutPos = (block[0] == '.'? 1: 0); TVector parts = SplitString(block.substr(cutPos), "."); WWW_CHECK(parts.size() >= 2, "too few parts in adhoc trace id '%s' block '%s'", id.data(), block.data()); auto blockPb = Cfg.AddBlocks(); auto pdescPb = blockPb->MutableProbeDesc(); if (parts[0] == "Group") { pdescPb->SetGroup(parts[1]); } else { pdescPb->SetProvider(parts[0]); pdescPb->SetName(parts[1]); } bool defaultAction = true; for (auto i = parts.begin() + 2, e = parts.end(); i != e; ++i) { const TString& part = *i; if (part.empty()) { continue; } switch (part[0]) { case 'l': Cfg.SetPerThreadLogSize(FromString(part.substr(1))); break; case 'd': Cfg.SetLogDurationUs(ParseDuration(part.substr(1))); break; case 's': blockPb->MutablePredicate()->SetSampleRate(1.0 / Max(1, FromString(part.substr(1)))); break; case 'p': ParsePredicate(blockPb->MutablePredicate()->AddOperators(), part.substr(1)); break; case 'a': ParseAction(blockPb->AddAction(), part.substr(1)); defaultAction = false; break; default: WWW_CHECK(false, "unknown adhoc trace part type '%s' in '%s'", part.data(), id.data()); } } if (defaultAction) { blockPb->AddAction()->MutableLogAction(); } } return true; } return false; } private: static bool IsAdHocId(const TString& id) { return !id.empty() && id[0] == '.'; } void ParsePredicate(NLWTrace::TOperator* op, const TString& p) { size_t sign = p.find_first_of("=!><"); WWW_CHECK(sign != TString::npos, "wrong predicate format in adhoc trace: %s", p.data()); op->AddArgument()->SetParam(p.substr(0, sign)); size_t value = sign + 1; switch (p[sign]) { case '=': op->SetType(NLWTrace::OT_EQ); break; case '!': { WWW_CHECK(p.size() > sign + 1, "wrong predicate operator format in adhoc trace: %s", p.data()); WWW_CHECK(p[sign + 1] == '=', "wrong predicate operator format in adhoc trace: %s", p.data()); value++; op->SetType(NLWTrace::OT_NE); break; } case '<': { WWW_CHECK(p.size() > sign + 1, "wrong predicate operator format in adhoc trace: %s", p.data()); if (p[sign + 1] == '=') { value++; op->SetType(NLWTrace::OT_LE); } else { op->SetType(NLWTrace::OT_LT); } break; } case '>': { WWW_CHECK(p.size() > sign + 1, "wrong predicate operator format in adhoc trace: %s", p.data()); if (p[sign + 1] == '=') { value++; op->SetType(NLWTrace::OT_GE); } else { op->SetType(NLWTrace::OT_GT); } break; } default: WWW_CHECK(false, "wrong predicate operator format in adhoc trace: %s", p.data()); } op->AddArgument()->SetValue(p.substr(value)); } void ParseAction(NLWTrace::TAction* action, const TString& a) { // NOTE: checks for longer action names should go first, your captain. if (a.substr(0, 3) == "lsr") { auto pb = action->MutableRunLogShuttleAction(); for (const TString& opt : SplitString(a.substr(3), "-")) { if (!opt.empty()) { switch (opt[0]) { case 'i': pb->SetIgnore(true); break; case 's': pb->SetShuttlesCount(FromString(opt.substr(1))); break; case 't': pb->SetMaxTrackLength(FromString(opt.substr(1))); break; default: WWW_CHECK(false, "unknown adhoc trace log shuttle opt '%s' in '%s'", opt.data(), a.data()); } } } } else if (a.substr(0, 3) == "lse") { auto pb = action->MutableEditLogShuttleAction(); for (const TString& opt : SplitString(a.substr(3), "-")) { if (!opt.empty()) { switch (opt[0]) { case 'i': pb->SetIgnore(true); break; default: WWW_CHECK(false, "unknown adhoc trace log shuttle opt '%s' in '%s'", opt.data(), a.data()); } } } } else if (a.substr(0, 3) == "lsd") { action->MutableDropLogShuttleAction(); } else if (a.substr(0, 1) == "l") { auto pb = action->MutableLogAction(); for (const TString& opt : SplitString(a.substr(1), "-")) { if (!opt.empty()) { switch (opt[0]) { case 't': pb->SetLogTimestamp(true); break; case 'r': pb->SetMaxRecords(FromString(opt.substr(1))); break; default: WWW_CHECK(false, "unknown adhoc trace log opt '%s' in '%s'", opt.data(), a.data()); } } } } else { WWW_CHECK(false, "wrong action format in adhoc trace: %s", a.data()); } } static ui64 ParseDuration(const TString& s) { if (s.substr(s.length() - 2) == "us") return FromString(s.substr(0, s.length() - 2)); if (s.substr(s.length() - 2) == "ms") return FromString(s.substr(0, s.length() - 2)) * 1000; if (s.substr(s.length() - 1) == "s") return FromString(s.substr(0, s.length() - 1)) * 1000 * 1000; if (s.substr(s.length() - 1) == "m") return FromString(s.substr(0, s.length() - 1)) * 60 * 1000 * 1000; else return FromString(s); } }; // Class that maintains one thread iff there are adhoc traces and cleans'em by deadlines class TTraceCleaner { private: NLWTrace::TManager* TraceMngr; TAtomic Quit = 0; TMutex Mtx; TCondVar WakeCondVar; THashMap Deadlines; volatile bool ThreadIsRunning = false; THolder Thread; public: TTraceCleaner(NLWTrace::TManager* traceMngr) : TraceMngr(traceMngr) {} ~TTraceCleaner() { AtomicSet(Quit, 1); WakeCondVar.Signal(); // TThread dtor joins thread } // Returns deadline for specified trace id or zero TInstant GetDeadline(const TString& id) const { TGuard g(Mtx); auto iter = Deadlines.find(id); return iter != Deadlines.end()? iter->second: TInstant::Zero(); } // Postpone deletion of specified trace for specified timeout void Postpone(const TString& id, TDuration timeout, bool allowLowering) { TGuard g(Mtx); TInstant newDeadline = TInstant::Now() + timeout; if (Deadlines[id] < newDeadline) { Deadlines[id] = newDeadline; } else if (allowLowering) { // Deadline lowering requires wake Deadlines[id] = newDeadline; WakeCondVar.Signal(); } if (newDeadline != TInstant::Max() && !ThreadIsRunning) { // Note that dtor joins previous thread if any Thread.Reset(new TThread(ThreadProc, this)); Thread->Start(); ThreadIsRunning = true; } } // Forget about specified trace deletion void Forget(const TString& id) { TGuard g(Mtx); Deadlines.erase(id); WakeCondVar.Signal(); // in case thread is not required any more } private: void Exec() { while (!AtomicGet(Quit)) { TGuard g(Mtx); // Delete all timed out traces TInstant now = TInstant::Now(); TInstant nextDeadline = TInstant::Max(); for (auto i = Deadlines.begin(), e = Deadlines.end(); i != e;) { const TString& id = i->first; TInstant deadline = i->second; if (deadline < now) { try { TraceMngr->Delete(id); } catch (...) { // already deleted } Deadlines.erase(i++); } else { nextDeadline = Min(nextDeadline, deadline); ++i; } } // Stop thread if there is no more work if (Deadlines.empty() || nextDeadline == TInstant::Max()) { ThreadIsRunning = false; break; } // Wait until next deadline or quit WakeCondVar.WaitD(Mtx, nextDeadline); } } static void* ThreadProc(void* _this) { TString name = "LWTraceCleaner"; // Copy-pasted from kikimr/core/util/thread.h #if defined(_linux_) TStringStream linuxName; linuxName << TStringBuf(GetExecPath()).RNextTok('/') << "." << name; TThread::SetCurrentThreadName(linuxName.Str().data()); #else TThread::SetCurrentThreadName(name.data()); #endif static_cast(_this)->Exec(); return nullptr; } }; class TChromeTrace { private: TMultiMap TraceEvents; THashMap Tids; public: void Add(TThread::TId tid, ui64 tsCycles, const TString& ph, const TString& cat, const NLWTrace::TLogItem* argsItem = nullptr, const TString& name = TString(), const TString& id = TString()) { auto tidIter = Tids.find(tid); if (tidIter == Tids.end()) { tidIter = Tids.emplace(tid, ToString(Tids.size() + 1)).first; } const TString& shortId = tidIter->second; double ts = Timestamp(tsCycles); TraceEvents.emplace(ts, Event(shortId, ts, ph, cat, argsItem, name, id)); } void Output(IOutputStream& os) { os << "{\"traceEvents\":["; bool first = true; for (auto kv : TraceEvents) { if (!first) { os << ",\n"; } os << kv.second; first = false; } os << "]}"; } private: static TString Event(const TString& tid, double ts, const TString& ph, const TString& cat, const NLWTrace::TLogItem* argsItem, const TString& name, const TString& id) { TStringStream ss; pid_t pid = 1; ss << "{\"pid\":" << pid << ",\"tid\":" << tid << ",\"ts\":" << Sprintf("%lf", ts) << ",\"ph\":\"" << ph << "\"" << ",\"cat\":\"" << cat << "\""; if (name) { ss << ",\"name\":\"" << name << "\""; } if (id) { ss << ",\"id\":\"" << id << "\""; } if (argsItem && argsItem->SavedParamsCount > 0) { ss << ",\"args\":{"; TString paramValues[LWTRACE_MAX_PARAMS]; argsItem->Probe->Event.Signature.SerializeParams(argsItem->Params, paramValues); bool first = true; for (size_t pi = 0; pi < argsItem->SavedParamsCount; pi++, first = false) { if (!first) { ss << ","; } ss << "\"" << TString(argsItem->Probe->Event.Signature.ParamNames[pi]) << "\":" "\"" << paramValues[pi] << "\""; } ss << "}"; } ss << "}"; return ss.Str(); } static double Timestamp(ui64 cycles) { return double(cycles) * 1000000.0 / NHPTimer::GetClockRate(); } }; TString MakeUrl(const TCgiParameters& e, const THashMap& values) { TStringStream ss; bool first = true; for (const auto& [k, v] : e) { if (values.find(k) == values.end()) { ss << (first? "?": "&") << k << "=" << v; first = false; } } for (const auto& [k, v] : values) { ss << (first? "?": "&") << k << "=" << v; first = false; } return ss.Str(); } TString MakeUrl(const TCgiParameters& e, const TString& key, const TString& value, bool keep = false) { TStringStream ss; bool first = true; for (const auto& kv : e) { if (keep || kv.first != key) { ss << (first? "?": "&") << kv.first << "=" << kv.second; first = false; } } ss << (first? "?": "&") << key << "=" << value; return ss.Str(); } TString MakeUrlAdd(const TCgiParameters& e, const TString& key, const TString& value) { TStringStream ss; bool first = true; for (const auto& kv : e) { ss << (first? "?": "&") << kv.first << "=" << kv.second; first = false; } ss << (first? "?": "&") << key << "=" << value; return ss.Str(); } TString MakeUrlReplace(const TCgiParameters& e, const TString& key, const TString& oldValue, const TString& newValue) { TStringStream ss; bool first = true; bool inserted = false; for (const auto& kv : e) { if (kv.first == key && (kv.second == oldValue || kv.second == newValue)) { if (!inserted) { inserted = true; ss << (first? "?": "&") << key << "=" << newValue; first = false; } } else { ss << (first? "?": "&") << kv.first << "=" << kv.second; first = false; } } if (!inserted) { ss << (first? "?": "&") << key << "=" << newValue; } return ss.Str(); } TString MakeUrlErase(const TCgiParameters& e, const TString& key, const TString& value) { TStringStream ss; bool first = true; for (const auto& kv : e) { if (kv.first != key || kv.second != value) { ss << (first? "?": "&") << kv.first << "=" << kv.second; first = false; } } return ss.Str(); } TString EscapeSubvalue(const TString& s) { TString ret; ret.reserve(s.size()); for (size_t i = 0; i < s.size(); i++) { char c = s[i]; if (c == ':') { ret.append("^c"); } else if (c == '^') { ret.append("^^"); } else { ret.append(c); } } return ret; } TString UnescapeSubvalue(const TString& s) { TString ret; ret.reserve(s.size()); for (size_t i = 0; i < s.size(); i++) { char c = s[i]; if (c == '^' && i + 1 < s.size()) { char c2 = s[++i]; if (c2 == 'c') { ret.append(':'); } else if (c2 == '^') { ret.append('^'); } else { ret.append(c); ret.append(c2); } } else { ret.append(c); } } return ret; } TVector Subvalues(const TCgiParameters& e, const TString& key) { if (!e.Has(key)) { return TVector(); } else { TVector ret; for (const TString& s : SplitString(e.Get(key), ":", 0, KEEP_EMPTY_TOKENS)) { ret.push_back(UnescapeSubvalue(s)); } if (ret.empty()) { ret.push_back(""); } return ret; } } TString ParseTagsOut(const TString& taggedStr, TTags& tags) { auto vec = SplitString(taggedStr, "-"); if (vec.empty()) { return ""; } auto iter = vec.begin(); TString value = *iter++; for (;iter != vec.end(); ++iter) { tags.insert(*iter); } return value; } TString JoinTags(TTags tags) { return JoinStrings(TVector(tags.begin(), tags.end()), "-"); } TString MakeValue(const TVector& subvalues) { TVector subvaluesEsc; for (const TString& s : subvalues) { subvaluesEsc.push_back(EscapeSubvalue(s)); } return JoinStrings(subvaluesEsc, ":"); } TString MakeUrlAddSub(const TCgiParameters& e, const TString& key, const TString& subvalue) { const TString& value = e.Get(key); auto subvalues = Subvalues(e, key); subvalues.push_back(subvalue); return MakeUrlReplace(e, key, value, MakeValue(subvalues)); } TString MakeUrlReplaceSub(const TCgiParameters& e, const TString& key, const TString& oldSubvalue, const TString& newSubvalue) { const TString& value = e.Get(key); auto subvalues = Subvalues(e, key); auto iter = std::find(subvalues.begin(), subvalues.end(), oldSubvalue); if (iter != subvalues.end()) { *iter = newSubvalue; } else { subvalues.push_back(newSubvalue); } return MakeUrlReplace(e, key, value, MakeValue(subvalues)); } TString MakeUrlEraseSub(const TCgiParameters& e, const TString& key, const TString& subvalue) { const TString& value = e.Get(key); auto subvalues = Subvalues(e, key); auto iter = std::find(subvalues.begin(), subvalues.end(), subvalue); if (iter != subvalues.end()) { subvalues.erase(iter); } if (subvalues.empty()) { return MakeUrlErase(e, key, value); } else { return MakeUrlReplace(e, key, value, MakeValue(subvalues)); } } template TString UrlAdd(const TCgiParameters& e, const TString& key, const TString& value); template <> TString UrlAdd(const TCgiParameters& e, const TString& key, const TString& value) { return MakeUrlAdd(e, key, value); } template <> TString UrlAdd(const TCgiParameters& e, const TString& key, const TString& value) { return MakeUrlAddSub(e, key, value); } template TString UrlReplace(const TCgiParameters& e, const TString& key, const TString& oldValue, const TString& newValue); template <> TString UrlReplace(const TCgiParameters& e, const TString& key, const TString& oldValue, const TString& newValue) { return MakeUrlReplace(e, key, oldValue, newValue); } template <> TString UrlReplace(const TCgiParameters& e, const TString& key, const TString& oldValue, const TString& newValue) { return MakeUrlReplaceSub(e, key, oldValue, newValue); } template TString UrlErase(const TCgiParameters& e, const TString& key, const TString& value); template <> TString UrlErase(const TCgiParameters& e, const TString& key, const TString& value) { return MakeUrlErase(e, key, value); } template <> TString UrlErase(const TCgiParameters& e, const TString& key, const TString& value) { return MakeUrlEraseSub(e, key, value); } void OutputCommonHeader(IOutputStream& out) { out << NResource::Find("lwtrace/mon/static/header.html") << Endl; } void OutputCommonFooter(IOutputStream& out) { out << NResource::Find("lwtrace/mon/static/footer.html") << Endl; } struct TScopedHtmlInner { explicit TScopedHtmlInner(IOutputStream& str) : Str(str) { Str << "\n" ""; HTML(str) { HEAD() { OutputCommonHeader(Str); } } Str << ""; } ~TScopedHtmlInner() { OutputCommonFooter(Str); Str << ""; } inline operator bool () const noexcept { return true; } IOutputStream &Str; }; TString NavbarHeader() { return "
" "LWTrace" "
"; } struct TSelectorsContainer { TSelectorsContainer(IOutputStream& str) : Str(str) { Str << ""; } catch(...) {} } IOutputStream& Str; }; struct TNullContainer { TNullContainer(IOutputStream&) {} }; class TPageGenBase: public std::exception {}; template class TPageGen: public TPageGenBase { private: TString Content; TString HttpResponse; public: void BuildResponse() { TStringStream ss; WWW_HTML(ss) { TContainer container(ss); ss << Content; } HttpResponse = ss.Str(); } explicit TPageGen(const TString& content = TString()) : Content(content) { BuildResponse(); } void Append(const TString& moreContent) { Content.append(moreContent); BuildResponse(); } void Prepend(const TString& moreContent) { Content.prepend(moreContent); BuildResponse(); } virtual const char* what() const noexcept { return HttpResponse.data(); } operator bool() const { return !Content.empty(); } }; enum EStyleFlags { // bit 1 Link = 0x0, Button = 0x1, // bit 2 NonErasable = 0x0, Erasable = 0x2, // bit 3-4 Medium = 0x0, Large = 0x4, Small = 0x8, ExtraSmall = 0xC, SizeMask = 0xC, // bit 5 NoCaret = 0x0, Caret = 0x10, // bit 6 SimpleValue = 0x0, CompositeValue = 0x20 }; template TString BtnClass() { if ((flags & SizeMask) == Large) { return "btn btn-lg"; } else if ((flags & SizeMask) == Small) { return "btn btn-sm"; } else if ((flags & SizeMask) == ExtraSmall) { return "btn btn-xs"; } return "btn"; } void SelectorTitle(IOutputStream& os, const TString& text) { if (!text.empty()) { os << text; } } template void BtnHref(IOutputStream& os, const TString& text, const TString& href, bool push = false) { if (flags & Button) { os << ""; } else { os << "" << text << ""; } } void DropdownBeginSublist(IOutputStream& os, const TString& text) { os << "
  • " << text << "
      "; } void DropdownEndSublist(IOutputStream& os) { os << "
  • "; } void DropdownItem(IOutputStream& os, const TString& text, const TString& href, bool separated = false) { if (separated) { os << "
  • "; } os << "
  • " << text << "
  • "; } TString SuggestSelection() { return "--- "; } TString RemoveSelection() { return "Remove"; } TString GetDescription(const TString& value, const TVariants& variants) { for (const auto& var : variants) { if (value == var.first) { return var.second; } } if (!value) { return SuggestSelection(); } return value; } template void DropdownSelector(IOutputStream& os, const TCgiParameters& e, const TString& param, const TString& value, const TString& text, const TVariants& variants, const TString& realValue = TString()) { HTML(os) { SelectorTitle(os, text); os << "
    "; if (flags & Button) { os << ""; } else { os <<""; } UL_CLASS ("dropdown-menu") { for (const auto& var : variants) { DropdownItem(os, var.second, UrlReplace(e, param, value, var.first)); } if (flags & Erasable) { DropdownItem(os, RemoveSelection(), UrlErase(e, param, value), true); } } os << "
    "; } } void RequireSelection(TStringStream& ss, const TCgiParameters& e, const TString& param, const TString& text, const TVariants& variants) { const TString& value = e.Get(param); DropdownSelector(ss, e, param, value, text, variants); if (!value) { throw TPageGen(ss.Str()); } } void RequireMultipleSelection(TStringStream& ss, const TCgiParameters& e, const TString& param, const TString& text, const TVariants& variants) { SelectorTitle(ss, text); TSet selectedValues; for (const TString& subvalue : Subvalues(e, param)) { selectedValues.insert(subvalue); } for (const TString& subvalue : Subvalues(e, param)) { DropdownSelector(ss, e, param, subvalue, "", variants); } if (selectedValues.contains("")) { throw TPageGen(ss.Str()); } else { BtnHref(ss, "+", MakeUrlAddSub(e, param, "")); if (selectedValues.empty()) { throw TPageGen(ss.Str()); } } } void OptionalSelection(TStringStream& ss, const TCgiParameters& e, const TString& param, const TString& text, const TVariants& variants) { TSet selectedValues; for (const TString& subvalue : Subvalues(e, param)) { selectedValues.insert(subvalue); } if (!selectedValues.empty()) { SelectorTitle(ss, text); } for (const TString& subvalue : Subvalues(e, param)) { DropdownSelector(ss, e, param, subvalue, "", variants); } if (selectedValues.empty()) { BtnHref(ss, text, MakeUrlAddSub(e, param, "")); } } void OptionalMultipleSelection(TStringStream& ss, const TCgiParameters& e, const TString& param, const TString& text, const TVariants& variants) { TSet selectedValues; for (const TString& subvalue : Subvalues(e, param)) { selectedValues.insert(subvalue); } if (!selectedValues.empty()) { SelectorTitle(ss, text); } for (const TString& subvalue : Subvalues(e, param)) { DropdownSelector(ss, e, param, subvalue, "", variants); } if (selectedValues.contains("")) { throw TPageGen(ss.Str()); } else { BtnHref(ss, selectedValues.empty()? text: "+", MakeUrlAddSub(e, param, "")); } } TVariants ListColumns(const NAnalytics::TTable& table) { TSet cols; // bool addSpecialCols = false; // if (addSpecialCols) { // cols.insert("_count"); // } for (auto& row : table) { for (auto& kv : row) { cols.insert(kv.first); } } TVariants result; for (const auto& s : cols) { result.emplace_back(s, s); } return result; } TString TaggedValue(const TString& value, const TString& tag) { if (!tag) { return value; } return value + "-" + tag; } TVariants ValueVars(const TVariants& values, const TString& tag) { TVariants ret; for (auto& p : values) { ret.emplace_back(TaggedValue(p.first, tag), p.second); } return ret; } TVariants TagVars(const TString& value, const TVariants& tags) { TVariants ret; for (auto& p : tags) { ret.emplace_back(TaggedValue(value, p.first), p.second); } return ret; } TVariants SeriesTags() { TVariants ret; // MSVS2013 doesn't understand complex initializer lists ret.emplace_back("", "as is"); ret.emplace_back("stack", "cumulative"); return ret; } void SeriesSelectors(TStringStream& ss, const TCgiParameters& e, const TString& xparam, const TString& yparam, const NAnalytics::TTable& data) { TTags xtags; TString xn = ParseTagsOut(e.Get(xparam), xtags); DropdownSelector(ss, e, xparam, e.Get(xparam), "with Ox:", ValueVars(ListColumns(data), JoinTags(xtags))); if (xn) { DropdownSelector(ss, e, xparam, e.Get(xparam), "", TagVars(xn, SeriesTags())); } TString yns = e.Get(yparam); SelectorTitle(ss, "and Oy:"); bool first = true; bool hasEmpty = false; for (auto& subvalue : Subvalues(e, yparam)) { TTags ytags; TString yn = ParseTagsOut(subvalue, ytags); DropdownSelector(ss, e, yparam, subvalue, first? "": ", ", ValueVars(ListColumns(data), JoinTags(ytags))); if (yn) { DropdownSelector(ss, e, yparam, subvalue, "", TagVars(yn, SeriesTags())); } first = false; if (yn.empty()) { hasEmpty = true; } } if (hasEmpty) { throw TPageGen(ss.Str()); } else { BtnHref(ss, "+", MakeUrlAddSub(e, yparam, "")); } if (!xn || !yns) { throw TPageGen(ss.Str()); } } class TProbesHtmlPrinter { private: TVector> TableData; static constexpr int TimeoutSec = 15 * 60; // default timeout public: void Push(const NLWTrace::TProbe* probe) { TableData.emplace_back(); auto& row = TableData.back(); row.emplace_back(); TString& groups = row.back(); bool first = true; for (const char* const* i = probe->Event.Groups; *i != nullptr; ++i, first = false) { groups.append(TString(first? "": ", ") + GroupHtml(*i)); } row.push_back(ProbeHtml(probe->Event.GetProvider(), probe->Event.Name)); row.emplace_back(); TString& params = row.back(); first = true; for (size_t i = 0; i < probe->Event.Signature.ParamCount; i++, first = false) { params.append(TString(first? "": ", ") + probe->Event.Signature.ParamTypes[i] + " " + probe->Event.Signature.ParamNames[i]); } row.emplace_back(ToString(probe->GetExecutorsCount())); } void Output(IOutputStream& os) { HTML(os) { TABLE() { TABLEHEAD() { TABLEH() { os << "Groups"; } TABLEH() { os << "Name"; } TABLEH() { os << "Params"; } TABLEH() { os << "ExecCount"; } } TABLEBODY() { for (auto& row : TableData) { TABLER() { for (TString& cell : row) { TABLED() { os << cell; } } } } } } } } private: TString GroupHtml(const TString& group) { TStringStream ss; ss << ""; return ss.Str(); } TString ProbeHtml(const TString& provider, const TString& probe) { TStringStream ss; ss << ""; return ss.Str(); } }; void TDashboardRegistry::Register(const NLWTrace::TDashboard& dashboard) { TGuard g(Mutex); Dashboards[dashboard.GetName()] = dashboard; } void TDashboardRegistry::Register(const TVector& dashboards) { for (const auto& dashboard : dashboards) { Register(dashboard); } } void TDashboardRegistry::Register(const TString& dashText) { NLWTrace::TDashboard dash; if (!google::protobuf::TextFormat::ParseFromString(dashText, &dash)) { ythrow yexception() << "Couldn't parse into dashboard"; } Register(dash); } bool TDashboardRegistry::Get(const TString& name, NLWTrace::TDashboard& dash) { TGuard g(Mutex); if (!Dashboards.contains(name)) { return false; } dash = Dashboards[name]; return true; } void TDashboardRegistry::Output(TStringStream& ss) { HTML(ss) { TABLE() { TABLEHEAD() { TABLEH() { ss << "Name"; } } TABLEBODY() { TGuard g(Mutex); for (auto& kv : Dashboards) { const auto& dash = kv.second; TABLER() { TABLED() { ss << "" << dash.GetName() << ""; } } } } } } } class ILogSource { public: virtual ~ILogSource() {} virtual TString GetId() = 0; virtual TInstant GetStartTime() = 0; virtual TDuration GetTimeout(TInstant now) = 0; virtual ui64 GetEventsCount() = 0; virtual ui64 GetThreadsCount() = 0; }; class TTraceLogSource : public ILogSource { private: TString Id; const TTraceCleaner& Cleaner; const NLWTrace::TSession* Trace; public: TTraceLogSource(const TString& id, const NLWTrace::TSession* trace, const TTraceCleaner& cleaner) : Id(id) , Cleaner(cleaner) , Trace(trace) {} TString GetId() override { return Id; } TInstant GetStartTime() override { return Trace->GetStartTime(); } TDuration GetTimeout(TInstant now) override { TInstant deadline = Cleaner.GetDeadline(Id); if (deadline < now) { return TDuration::Zero(); } else if (deadline == TInstant::Max()) { return TDuration::Max(); } else { return deadline - now; } } ui64 GetEventsCount() override { return Trace->GetEventsCount(); } ui64 GetThreadsCount() override { return Trace->GetThreadsCount(); } }; class TSnapshotLogSource : public ILogSource { private: TString Sid; // Log should be used for read-only purpose, because it can be accessed from multiple threads // Atomic pointer is used to avoid thread-safety issues with snapshot deletion // (I hope protobuf const-implementation doesn't use any mutable non-thread-safe stuff inside) TAtomicSharedPtr Log; public: // Constructor should be called under SnapshotsMtx lock TSnapshotLogSource(const TString& sid, const TAtomicSharedPtr& log) : Sid(sid) , Log(log) {} TString GetId() override { return Sid + "~"; } TInstant GetStartTime() override { return TInstant::MicroSeconds(Log->GetCrtTime()); } TDuration GetTimeout(TInstant now) override { Y_UNUSED(now); return TDuration::Max(); } ui64 GetEventsCount() override { return Log->GetEventsCount(); } ui64 GetThreadsCount() override { return Log->ThreadLogsSize(); } }; class TLogSources { private: TTraceCleaner& Cleaner; TInstant Now; using TLogSourcePtr = std::unique_ptr; TMap LogSources; public: explicit TLogSources(TTraceCleaner& cleaner, TInstant now = TInstant::Now()) : Cleaner(cleaner) , Now(now) {} void Push(const TString& sid, const TAtomicSharedPtr& log) { TLogSourcePtr ls(new TSnapshotLogSource(sid, log)); LogSources.emplace(ls->GetId(), std::move(ls)); } void Push(const TString& id, const NLWTrace::TSession* trace) { TLogSourcePtr ls(new TTraceLogSource(id, trace, Cleaner)); LogSources.emplace(ls->GetId(), std::move(ls)); } template void ForEach(TFunc& func) { for (auto& kv : LogSources) { func.Push(kv.second.get()); } } template void ForEach(TFunc& func) const { for (const auto& kv : LogSources) { func.Push(kv.second.get()); } } }; class TTracesHtmlPrinter { private: IOutputStream& Os; TInstant Now; public: explicit TTracesHtmlPrinter(IOutputStream& os) : Os(os) , Now(TInstant::Now()) {} void Push(ILogSource* src) { TString id = src->GetId(); Os << ""; Os << ""; try { Os << src->GetStartTime().ToStringUpToSeconds(); } catch (...) { Os << "error: " << CurrentExceptionMessage(); } Os << "" << "" << "" << EncodeHtmlPcdata(id) << "" << "" << src->GetEventsCount() << "" << "" << src->GetThreadsCount() << "" << "Text" << "Json" << "Query" << "Analytics" << "
    " // navbar-right is hack to drop left "Modify" "" "
    " << "\n"; } private: static TString TimeoutToString(TDuration d) { TStringStream ss; if (d == TDuration::Zero()) { ss << "0"; } else if (d == TDuration::Max()) { ss << "-"; } else { ui64 us = d.GetValue(); ui64 ms = us / 1000; ui64 sec = ms / 1000; ui64 min = sec / 60; ui64 hours = min / 60; ui64 days = hours / 24; ui64 weeks = days / 7; us -= ms * 1000; ms -= sec * 1000; sec -= min * 60; min -= hours * 60; hours -= days * 24; days -= weeks * 7; int terms = 0; if ((terms > 0 && terms < 2) || ( terms == 0 && weeks)) { ss << (ss.Str()? " ": "") << weeks << "w"; terms++; } if ((terms > 0 && terms < 2) || ( terms == 0 && days)) { ss << (ss.Str()? " ": "") << days << "d"; terms++; } if ((terms > 0 && terms < 2) || ( terms == 0 && hours)) { ss << (ss.Str()? " ": "") << hours << "h"; terms++; } if ((terms > 0 && terms < 2) || ( terms == 0 && min)) { ss << (ss.Str()? " ": "") << min << "m"; terms++; } if ((terms > 0 && terms < 2) || ( terms == 0 && sec)) { ss << (ss.Str()? " ": "") << sec << "s"; terms++; } if ((terms > 0 && terms < 2) || ( terms == 0 && ms)) { ss << (ss.Str()? " ": "") << ms << "ms"; terms++; } if ((terms > 0 && terms < 2) || ( terms == 0 && us)) { ss << (ss.Str()? " ": "") << us << "us"; terms++; } } return ss.Str(); } }; class TTracesLister { private: TVariants& Variants; public: TTracesLister(TVariants& variants) : Variants(variants) {} void Push(ILogSource* src) { Variants.emplace_back(src->GetId(), src->GetId()); } }; TVariants ListTraces(const TLogSources& srcs) { TVariants variants; TTracesLister lister(variants); srcs.ForEach(lister); return variants; } class TTimestampCutter { private: THashMap> CutTsForThread; // tid -> time of first item mutable ui64 CutTsMax = 0; mutable TInstant CutInstantMax; bool Enabled; ui64 NowTs; public: explicit TTimestampCutter(bool enabled) : Enabled(enabled) , NowTs(GetCycleCount()) {} void Push(TThread::TId tid, const NLWTrace::TLogItem& item) { auto it = CutTsForThread.find(tid); if (it != CutTsForThread.end()) { ui64& ts = it->second.first; TInstant& inst = it->second.second; ts = Min(ts, item.TimestampCycles); inst = Min(inst, item.Timestamp); } else { CutTsForThread[tid] = std::make_pair(item.TimestampCycles, item.Timestamp); } } // Timestamp from which we are ensured that cyclic log for every thread is not truncated // NOTE: should NOT be called from Push(tid, item) functions ui64 StartTimestamp() const { if (CutTsMax == 0) { FindStartTime(); } return CutTsMax; } ui64 NowTimestamp() const { return NowTs; } TInstant StartInstant() const { if (CutInstantMax == TInstant::Zero()) { FindStartTime(); } return CutInstantMax; } // Returns true iff item should be skipped to avoid surprizes bool Skip(const NLWTrace::TLogItem& item) const { return Enabled && item.TimestampCycles < StartTimestamp(); } private: void FindStartTime() const { for (auto& kv : CutTsForThread) { CutTsMax = Max(CutTsMax, kv.second.first); CutInstantMax = Max(CutInstantMax, kv.second.second); } } }; class TLogFilter { private: struct TFilter { TString ParamName; TString ParamValue; bool Parsed; TLogQuery Query; NLWTrace::TLiteral Value; explicit TFilter(const TString& text) { if (!text) { // Neither ParamName nor ParamValue is selected ParamName.clear(); ParamValue.clear(); Parsed = false; return; } size_t pos = text.find('='); if (pos == TString::npos) { // Only ParamName has been selected ParamName = text; ParamValue.clear(); Parsed = false; return; } // Both ParamName and ParamValue have been selected ParamValue = text.substr(pos + 1); ParamName = text.substr(0, pos); Parsed = true; Query = TLogQuery(ParamName); Value = NLWTrace::TLiteral(ParamValue); } }; TVector Filters; THashSet Signatures; // Just to list param names TVariants ParamNames; THashMap> FilteredParamValues; // paramName -> { paramValue } public: explicit TLogFilter(const TVector& filters) { for (const TString& subvalue : filters) { TFilter filter(subvalue); FilteredParamValues[filter.ParamName]; // just create empty set to gather values later if (filter.Parsed) { Filters.push_back(filter); } } } virtual ~TLogFilter() {} template bool Filter(const TLog& log) { Gather(log); for (const TFilter& filter : Filters) { if (filter.Query.ExecuteQuery(log) != filter.Value) { return false; } } return true; } void FilterSelectors(TStringStream& ss, const TCgiParameters& e, const TString& fparam) { bool first = true; bool allParsed = true; for (const TString& subvalue : Subvalues(e, fparam)) { TFilter filter(subvalue); allParsed = allParsed && filter.Parsed; if (first) { SelectorTitle(ss, "where"); } DropdownSelector( ss, e, fparam, subvalue, first? "": ", ", ListParamNames(), filter.ParamName ); if (filter.ParamName) { DropdownSelector( ss, e, fparam, subvalue, "=", ListParamValues(filter.ParamName), filter.ParamValue? (filter.ParamName + "=" + filter.ParamValue): "" ); } first = false; } if (!allParsed) { throw TPageGen(ss.Str()); } else { BtnHref(ss, first? "where": "+", MakeUrlAddSub(e, fparam, "")); } } const TVariants& ListParamNames() { if (ParamNames.empty()) { THashSet paramNames; for (const NLWTrace::TSignature* sgn: Signatures) { for (size_t pi = 0; pi < sgn->ParamCount; pi++) { paramNames.insert(sgn->ParamNames[pi]); } } for (auto& pn : paramNames) { ParamNames.emplace_back(pn, pn); } } return ParamNames; } bool IsFiltered(const TString& paramName) const { return FilteredParamValues.contains(paramName); } private: // Gather param names and values for selectors void Gather(const NLWTrace::TLogItem& item) { Signatures.insert(&item.Probe->Event.Signature); if (!FilteredParamValues.empty() && item.SavedParamsCount > 0) { TString paramValues[LWTRACE_MAX_PARAMS]; item.Probe->Event.Signature.SerializeParams(item.Params, paramValues); for (size_t pi = 0; pi < item.SavedParamsCount; pi++) { auto iter = FilteredParamValues.find(item.Probe->Event.Signature.ParamNames[pi]); if (iter != FilteredParamValues.end()) { iter->second.insert(paramValues[pi]); } } } } void Gather(const NLWTrace::TTrackLog& tl) { for (const NLWTrace::TLogItem& item : tl.Items) { Gather(item); } } TVariants ListParamValues(const TString& paramName) const { TVariants result; auto iter = FilteredParamValues.find(paramName); if (iter != FilteredParamValues.end()) { for (const TString& paramValue : iter->second) { result.emplace_back(paramName + "=" + paramValue, paramValue); } } Sort(result.begin(), result.end()); return result; } }; static void EscapeJSONString(IOutputStream& os, const TString& s) { for (TString::const_iterator i = s.begin(), e = s.end(); i != e; ++i) { char c = *i; if (c < ' ') { os << Sprintf("\\u%04x", int(c)); } else if (c == '"') { os << "\\\""; } else if (c == '\\') { os << "\\\\"; } else { os << c; } } } static TString EscapeJSONString(const TString& s) { TStringStream ss; EscapeJSONString(ss, s); return ss.Str(); } class TLogJsonPrinter { private: IOutputStream& Os; bool FirstThread; bool FirstItem; public: explicit TLogJsonPrinter(IOutputStream& os) : Os(os) , FirstThread(true) , FirstItem(true) {} void OutputHeader() { Os << "{\n\t\"source\": \"" << HostName() << "\"" "\n\t, \"items\": [" ; } void OutputFooter(const NLWTrace::TSession* trace) { Os << "\n\t\t]" "\n\t, \"threads\": [" ; trace->ReadThreads(*this); Os << "]" "\n\t, \"events_count\": " << trace->GetEventsCount() << "\n\t, \"threads_count\": " << trace->GetThreadsCount() << "\n\t, \"timestamp\": " << Now().GetValue() << "\n}" ; } void PushThread(TThread::TId tid) { Os << (FirstThread? "": ", ") << tid; FirstThread = false; } void Push(TThread::TId tid, const NLWTrace::TLogItem& item) { Os << "\n\t\t" << (FirstItem? "": ", "); FirstItem = false; Os << "[" << tid << ", " << item.Timestamp.GetValue() << ", \"" << item.Probe->Event.GetProvider() << "\"" ", \"" << item.Probe->Event.Name << "\"" ", {" ; if (item.SavedParamsCount > 0) { TString ParamValues[LWTRACE_MAX_PARAMS]; item.Probe->Event.Signature.SerializeParams(item.Params, ParamValues); bool first = true; for (size_t i = 0; i < item.SavedParamsCount; i++, first = false) { Os << (first? "": ", ") << "\"" << item.Probe->Event.Signature.ParamNames[i] << "\": \""; EscapeJSONString(Os, ParamValues[i]); Os << "\""; } } Os << "}]"; } }; class TLogTextPrinter : public TLogFilter { private: TMultiMap > Items; TMultiMap Depot; THashMap ProbeId; TVector Probes; TTimestampCutter CutTs; TLogQuery Order; bool ReverseOrder = false; ui64 Head = 0; ui64 Tail = 0; bool ShowTs = false; public: TLogTextPrinter(const TVector& filters, ui64 head, ui64 tail, const TString& order, bool reverseOrder, bool cutTs, bool showTs) : TLogFilter(filters) , CutTs(cutTs) , Order(order) , ReverseOrder(reverseOrder) , Head(head) , Tail(tail) , ShowTs(showTs) {} TLogTextPrinter(const TCgiParameters& e) : TLogTextPrinter( Subvalues(e, "f"), e.Has("head")? FromString(e.Get("head")): 0, e.Has("tail")? FromString(e.Get("tail")): 0, e.Get("s"), e.Get("reverse") == "y", e.Get("cutts") == "y", e.Get("showts") == "y") {} enum EFormat { Text, Json }; void Output(IOutputStream& os) const { OutputItems(os); OutputDepot(os); } void OutputJson(IOutputStream& os) const { os << "{\"depot\":[\n"; OutputItems(os); OutputDepot(os); os << "],\"probes\":["; bool first = true; for (const NLWTrace::TProbe* probe : Probes) { os << (first? "": ",") << "{\"provider\":\"" << probe->Event.GetProvider() << "\",\"name\":\"" << probe->Event.Name << "\"}"; first = false; } os << "]}"; } NLWTrace::TTypedParam GetKey(const NLWTrace::TLogItem& item) { return Order? Order.ExecuteQuery(item): NLWTrace::TTypedParam(item.GetTimestampCycles()); } NLWTrace::TTypedParam GetKey(const NLWTrace::TTrackLog& tl) { return Order? Order.ExecuteQuery(tl): NLWTrace::TTypedParam(tl.GetTimestampCycles()); } void Push(TThread::TId tid, const NLWTrace::TLogItem& item) { CutTs.Push(tid, item); if (Filter(item)) { AddId(item); Items.emplace(GetKey(item), std::make_pair(tid, item)); } } void Push(TThread::TId tid, const NLWTrace::TTrackLog& tl) { Y_UNUSED(tid); if (Filter(tl)) { AddId(tl); Depot.emplace(GetKey(tl), tl); } } private: void AddId(const NLWTrace::TLogItem& item) { if (ProbeId.find(item.Probe) == ProbeId.end()) { size_t id = Probes.size(); ProbeId[item.Probe] = id; Probes.emplace_back(item.Probe); } } void AddId(const NLWTrace::TTrackLog& tl) { for (const auto& item : tl.Items) { AddId(item); } } bool HeadTailFilter(ui64 idx, ui64 size) const { bool headOk = idx < Head; bool tailOk = size < Tail + idx + 1ull; if (Head && Tail) { return headOk || tailOk; } else if (Head) { return headOk; } else if (Tail) { return tailOk; } else { return true; } } template void OutputItems(IOutputStream& os) const { ui64 idx = 0; ui64 size = Items.size(); ui64 startTs = ShowTs? CutTs.StartTimestamp(): 0; ui64 prevTs = 0; bool first = true; if (!ReverseOrder) { for (auto i = Items.begin(), e = Items.end(); i != e; ++i, idx++) { if (HeadTailFilter(idx, size)) { OutputItem(os, i->second.first, i->second.second, startTs, prevTs, first); prevTs = startTs? i->second.second.GetTimestampCycles(): 0; } } } else { for (auto i = Items.rbegin(), e = Items.rend(); i != e; ++i, idx++) { if (HeadTailFilter(idx, size)) { OutputItem(os, i->second.first, i->second.second, startTs, prevTs, first); prevTs = startTs? i->second.second.GetTimestampCycles(): 0; } } } } template void OutputDepot(IOutputStream& os) const { ui64 idx = 0; ui64 size = Depot.size(); bool first = true; if (!ReverseOrder) { for (auto i = Depot.begin(), e = Depot.end(); i != e; ++i, idx++) { if (HeadTailFilter(idx, size)) { OutputTrackLog(os, i->second, first); } } } else { for (auto i = Depot.rbegin(), e = Depot.rend(); i != e; ++i, idx++) { if (HeadTailFilter(idx, size)) { OutputTrackLog(os, i->second, first); } } } } template void OutputItem(IOutputStream& os, TThread::TId tid, const NLWTrace::TLogItem& item, ui64 startTs, ui64 prevTs, bool& first) const { if (CutTs.Skip(item)) { return; } if constexpr (Format == Text) { if (startTs) { if (!prevTs) { prevTs = item.GetTimestampCycles(); } os << Sprintf("%10.3lf %+10.3lf ms ", NHPTimer::GetSeconds(item.GetTimestampCycles() - startTs) * 1000.0, NHPTimer::GetSeconds(item.GetTimestampCycles() - prevTs) * 1000.0); } if (tid) { os << "<" << tid << "> "; } if (item.Timestamp != TInstant::Zero()) { os << "[" << item.Timestamp << "] "; } else { os << "[" << item.TimestampCycles << "] "; } os << GetProbeName(item.Probe) << "("; if (item.SavedParamsCount > 0) { TString ParamValues[LWTRACE_MAX_PARAMS]; item.Probe->Event.Signature.SerializeParams(item.Params, ParamValues); bool first = true; for (size_t i = 0; i < item.SavedParamsCount; i++, first = false) { os << (first? "": ", ") << item.Probe->Event.Signature.ParamNames[i] << "='" << EscapeC(ParamValues[i]) << "'"; } } os << ")\n"; } else if constexpr (Format == Json) { if (auto probeId = ProbeId.find(item.Probe); probeId != ProbeId.end()) { os << (first? "": ",") << (AsTrack? "[":"") << "[\"" << tid << "\",\""; if (item.Timestamp != TInstant::Zero()) { os << item.Timestamp.MicroSeconds(); } else { os << Sprintf("%.3lf", NHPTimer::GetSeconds(item.TimestampCycles) * 1e9); } os << "\"," << probeId->second << ",{"; if (item.SavedParamsCount > 0) { TString ParamValues[LWTRACE_MAX_PARAMS]; item.Probe->Event.Signature.SerializeParams(item.Params, ParamValues); bool first = true; for (size_t i = 0; i < item.SavedParamsCount; i++, first = false) { os << (first? "": ",") << "\"" << item.Probe->Event.Signature.ParamNames[i] << "\":\""; EscapeJSONString(os, ParamValues[i]); os << "\""; } } os << "}]" << (AsTrack? "]":""); } } first = false; } template void OutputTrackLog(IOutputStream& os, const NLWTrace::TTrackLog& tl, bool& first) const { if constexpr (Format == Json) { os << (first? "": ",") << "["; } first = false; ui64 prevTs = tl.GetTimestampCycles(); bool firstItem = true; for (const NLWTrace::TTrackLog::TItem& item: tl.Items) { OutputItem(os, item.ThreadId, item, tl.GetTimestampCycles(), prevTs, firstItem); prevTs = item.GetTimestampCycles(); } if constexpr (Format == Json) { os << "]"; } os << "\n"; } }; class TLogAnalyzer: public TLogFilter { private: TMultiMap> Items; TVector Depot; THashMap Groups; NAnalytics::TTable Table; bool TableCreated = false; TVector GroupBy; TTimestampCutter CutTs; public: TLogAnalyzer(const TVector& filters, const TVector& groupBy, bool cutTs) : TLogFilter(filters) , CutTs(cutTs) { for (const TString& groupParam : groupBy) { GroupBy.push_back(groupParam); } } const NAnalytics::TTable& GetTable() { if (!TableCreated) { TableCreated = true; if (GroupBy.empty()) { for (auto i = Items.begin(), e = Items.end(); i != e; ++i) { ParseItems(i->second.first, i->second.second); } ParseDepot(); } else { for (auto i = Items.begin(), e = Items.end(); i != e; ++i) { Map(i->second.first, i->second.second); } Reduce(); } } return Table; } void Push(TThread::TId tid, const NLWTrace::TLogItem& item) { CutTs.Push(tid, item); if (Filter(item)) { Items.emplace(item.TimestampCycles, std::make_pair(tid, item)); } } void Push(TThread::TId, const NLWTrace::TTrackLog& tl) { if (Filter(tl)) { Depot.emplace_back(tl); } } private: void FillRow(NAnalytics::TRow& row, const NLWTrace::TLogItem& item) { if (item.SavedParamsCount > 0) { TString paramValues[LWTRACE_MAX_PARAMS]; item.Probe->Event.Signature.SerializeParams(item.Params, paramValues); for (size_t i = 0; i < item.SavedParamsCount; i++) { double value = FromString(paramValues[i].data(), paramValues[i].size(), NAN); // If value cannot be cast to double or is inf/nan -- assume it's a string if (isfinite(value)) { row[item.Probe->Event.Signature.ParamNames[i]] = value; } else { row[item.Probe->Event.Signature.ParamNames[i]] = paramValues[i]; } } } } TString GetParam(const NLWTrace::TLogItem& item, TString* paramValues, const TString& paramName) { for (size_t pi = 0; pi < item.SavedParamsCount; pi++) { if (paramName == item.Probe->Event.Signature.ParamNames[pi]) { return paramValues[pi]; } } return TString(); } TString GetGroup(const NLWTrace::TLogItem& item, TString* paramValues) { TStringStream ss; bool first = true; for (const TString& groupParam : GroupBy) { ss << (first? "": "|") << GetParam(item, paramValues, groupParam); first = false; } return ss.Str(); } void ParseItems(TThread::TId tid, const NLWTrace::TLogItem& item) { if (CutTs.Skip(item)) { return; } Table.emplace_back(); NAnalytics::TRow& row = Table.back(); row["_thread"] = tid; if (item.Timestamp != TInstant::Zero()) { row["_wallTime"] = item.Timestamp.SecondsFloat(); row["_wallRTime"] = item.Timestamp.SecondsFloat() - CutTs.StartInstant().SecondsFloat(); } row["_cycles"] = item.TimestampCycles; row["_thrTime"] = CyclesToDuration((ui64)item.TimestampCycles).SecondsFloat(); row["_thrRTime"] = double(i64(item.TimestampCycles) - i64(CutTs.StartTimestamp())) / NHPTimer::GetCyclesPerSecond(); row["_thrNTime"] = double(i64(item.TimestampCycles) - i64(CutTs.NowTimestamp())) / NHPTimer::GetCyclesPerSecond(); row.Name = GetProbeName(item.Probe); FillRow(row, item); } void Map(TThread::TId tid, const NLWTrace::TLogItem& item) { if (item.SavedParamsCount > 0 && !CutTs.Skip(item)) { TString paramValues[LWTRACE_MAX_PARAMS]; item.Probe->Event.Signature.SerializeParams(item.Params, paramValues); TTrackLogRefs& tl = Groups[GetGroup(item, paramValues)]; tl.Items.emplace_back(tid, item); } } void Reduce() { for (auto& v : Groups) { const TString& group = v.first; const TTrackLogRefs& tl = v.second; Table.emplace_back(); NAnalytics::TRow& row = Table.back(); row.Name = group; for (const NLWTrace::TLogItem& item : tl.Items) { FillRow(row, item); } } } void ParseDepot() { for (NLWTrace::TTrackLog& tl : Depot) { Table.emplace_back(); NAnalytics::TRow& row = Table.back(); for (const NLWTrace::TLogItem& item : tl.Items) { FillRow(row, item); } } } }; struct TSampleOpts { bool ShowProvider = false; size_t SizeLimit = 50; }; enum ENodeType { NT_ROOT, NT_PROBE, NT_PARAM }; class TPatternTree; struct TPatternNode; struct TTrack : public TTrackLogRefs { TString TrackId; TPatternNode* LastNode = nullptr; }; using TTrackTr = TLogTraits; using TTrackIter = TTrackTr::const_iterator; // Visitor for tree traversing class IVisitor { public: virtual ~IVisitor() {} virtual void Visit(TPatternNode* node) = 0; }; // Per-node classifier class TClassifier { public: explicit TClassifier(TPatternNode* node, ENodeType childType, bool keepHead = false) : Node(node) , KeepHead(keepHead) , ChildType(childType) {} virtual ~TClassifier() {} virtual TPatternNode* Classify(TTrackIter cur, const TTrack& track) = 0; virtual void Accept(IVisitor* visitor) = 0; virtual bool IsLeaf() = 0; ENodeType GetChildType() const { return ChildType; } public: TPatternNode* Node; const bool KeepHead; ENodeType ChildType; }; // Track classification tree node struct TPatternNode { TString Name; TPatternNode* Parent = nullptr; THolder Classifier; struct TDesc { ENodeType Type = NT_ROOT; // NT_PROBE const NLWTrace::TProbe* Probe = nullptr; // NT_PARAM size_t Rollbacks = 0; TString ParamName; TString ParamValue; } Desc; ui64 TrackCount = 0; struct TTrackEntry { TTrack* Track; ui64 ResTotal; ui64 ResLast; TTrackEntry(TTrack* track, ui64 resTotal, ui64 resLast) : Track(track) , ResTotal(resTotal) , ResLast(resLast) {} }; TVector Tracks; ui64 ResTotalSum = 0; ui64 ResTotalMax = 0; TVector ResTotalAll; ui64 ResLastSum = 0; ui64 ResLastMax = 0; TVector ResLastAll; TVector TimelineSum; NAnalytics::TTable Slices; TString GetPath() const { if (Parent) { return Parent->GetPath() + Name; } return "/"; } NAnalytics::TTable GetTable() const { using namespace NAnalytics; NAnalytics::TTable ret; for (ui64 x : ResTotalAll) { ret.emplace_back(); TRow& row = ret.back(); row["resTotal"] = double(x) * 1000.0 / NHPTimer::GetClockRate(); } for (ui64 x : ResLastAll) { ret.emplace_back(); TRow& row = ret.back(); row["resLast"] = double(x) * 1000.0 / NHPTimer::GetClockRate(); } return ret; } template void OutputSample(const TString& bn, double b1, double b2, const TSampleOpts& opts, TReader& reader) const { bool filterTotal = false; if (bn == "resTotal") { filterTotal = true; } else { WWW_CHECK(bn == "resLast", "wrong sample filter param: %s", bn.data()); } size_t spaceLeft = opts.SizeLimit; for (const TTrackEntry& entry : Tracks) { const TTrack* track = entry.Track; // Filter out tracks that are not in sample if (filterTotal) { double resTotalMs = double(entry.ResTotal) * 1000.0 / NHPTimer::GetClockRate(); if (resTotalMs < b1 || resTotalMs > b2) { continue; } } else { double resLastMs = double(entry.ResLast) * 1000.0 / NHPTimer::GetClockRate(); if (resLastMs < b1 || resLastMs > b2) { continue; } } NLWTrace::TTrackLog tl; for (TTrackIter i = TTrackTr::begin(*track), e = TTrackTr::end(*track); i != e; ++i) { const NLWTrace::TLogItem& item = *i; const auto threadId = i->ThreadId; tl.Items.push_back(NLWTrace::TTrackLog::TItem(threadId, item)); } reader.Push(0, tl); if (spaceLeft) { spaceLeft--; if (!spaceLeft) { break; } } } } }; // Track classification tree class TPatternTree { public: // Per-node classifier by probe name class TClassifyByProbe : public TClassifier { private: using TChildren = THashMap; TChildren Children; TVector SortedChildren; public: explicit TClassifyByProbe(TPatternNode* node) : TClassifier(node, NT_PROBE) {} TPatternNode* Classify(TTrackIter cur, const TTrack& track) override { Y_UNUSED(track); const NLWTrace::TLogItem& item = *cur; TPatternNode* node = &Children[item.Probe]; node->Name = "/" + GetProbeName(item.Probe); node->Desc.Type = NT_PROBE; node->Desc.Probe = item.Probe; return node; } void Accept(IVisitor* visitor) override { if (SortedChildren.size() != Children.size()) { SortedChildren.clear(); SortedChildren.reserve(Children.size()); for (auto i = Children.begin(), e = Children.end(); i != e; ++i) { SortedChildren.push_back(&*i); } Sort(SortedChildren, [] (TChildren::value_type* lhs, TChildren::value_type* rhs) { NLWTrace::TProbe* lp = lhs->first; NLWTrace::TProbe* rp = rhs->first; if (int cmp = strcmp(lp->Event.GetProvider(), rp->Event.GetProvider())) { return cmp < 0; } return strcmp(lp->Event.Name, rp->Event.Name) < 0; }); } for (auto* kv : SortedChildren) { visitor->Visit(&kv->second); } } bool IsLeaf() override { return Children.empty(); } }; // Per-node classifier by probe param value class TClassifyByParam : public TClassifier { private: size_t Rollbacks; // How many items should we look back in track to locate probe TString ParamName; using TChildren = THashMap; TChildren Children; TVector SortedChildren; public: TClassifyByParam(TPatternNode* node, size_t rollbacks, const TString& paramName) : TClassifier(node, NT_PARAM, true) , Rollbacks(rollbacks) , ParamName(paramName) {} TPatternNode* Classify(TTrackIter cur, const TTrack& track) override { WWW_CHECK((i64)Rollbacks >= 0 && std::distance(TTrackTr::begin(track), cur) >= (i64)Rollbacks, "wrong rollbacks in node '%s'", Node->GetPath().data()); const NLWTrace::TLogItem& item = *(cur - Rollbacks); WWW_CHECK(item.SavedParamsCount > 0, "classify by params on probe w/o param loggging in node '%s'", Node->GetPath().data()); TString paramValues[LWTRACE_MAX_PARAMS]; TString* paramValue = nullptr; item.Probe->Event.Signature.SerializeParams(item.Params, paramValues); for (size_t pi = 0; pi < item.SavedParamsCount; pi++) { if (item.Probe->Event.Signature.ParamNames[pi] == ParamName) { paramValue = ¶mValues[pi]; } } WWW_CHECK(paramValue, "param '%s' not found in probe '%s' at path '%s'", ParamName.data(), GetProbeName(item.Probe).data(), Node->GetPath().data()); TPatternNode* node = &Children[*paramValue]; // Path example: "//Provider1.Probe1/Provider2.Probe2@1.xxx=123@2.type=harakiri" node->Name = "@" + ToString(Rollbacks) + "." + ParamName + "=" + *paramValue; node->Desc.Type = NT_PARAM; node->Desc.Rollbacks = Rollbacks; node->Desc.ParamName = ParamName; node->Desc.ParamValue = *paramValue; return node; } void Accept(IVisitor* visitor) override { if (SortedChildren.size() != Children.size()) { SortedChildren.clear(); SortedChildren.reserve(Children.size()); for (auto i = Children.begin(), e = Children.end(); i != e; ++i) { SortedChildren.push_back(&*i); } Sort(SortedChildren, [] (TChildren::value_type* lhs, TChildren::value_type* rhs) { return lhs->first < rhs->first; }); } for (auto* kv : SortedChildren) { visitor->Visit(&kv->second); } } bool IsLeaf() override { return Children.empty(); } }; private: TPatternNode Root; THashMap> ParamClassifiers; // path -> (rollbacks, param) TString SelectedPattern; TPatternNode* SelectedNode = nullptr; TVector Timeline; // Just to avoid reallocations public: TPatternTree(const TCgiParameters& e) { for (const TString& cl : Subvalues(e, "classify")) { size_t at = cl.find_last_of('@'); if (at != TString::npos) { size_t dot = cl.find('.', at + 1); if (dot != TString::npos) { size_t rollbacks = FromString(cl.substr(at + 1, dot - at - 1)); ParamClassifiers[cl.substr(0, at)] = std::make_pair(rollbacks, cl.substr(dot + 1)); } } } SelectedPattern = e.Get("pattern"); InitNode(&Root, nullptr); } TPatternNode* GetSelectedNode() { return SelectedNode; } NAnalytics::TTable GetSelectedTable() { if (SelectedNode) { return SelectedNode->GetTable(); } else { return NAnalytics::TTable(); } } template void OutputSelectedSample(const TString& bn, double b1, double b2, const TSampleOpts& opts, TReader& reader) { if (SelectedNode) { SelectedNode->OutputSample(bn, b1, b2, opts, reader); } } // Register track in given node void AddTrackToNode(TPatternNode* node, TTrack& track, ui64 resTotal, TVector& timeline) { if (!SelectedNode) { if (node->GetPath() == SelectedPattern) { SelectedNode = node; } } // Counting node->TrackCount++; // Resource total node->ResTotalSum += resTotal; node->ResTotalMax = Max(node->ResTotalMax, resTotal); node->ResTotalAll.push_back(resTotal); // Resource last ui64 resLast = 0; resLast = resTotal - (timeline.size() < 2? 0: timeline[timeline.size() - 2]); node->ResLastSum += resLast; node->ResLastMax = Max(node->ResLastMax, resLast); node->ResLastAll.push_back(resLast); // Timeline if (node->TimelineSum.size() < timeline.size()) { node->TimelineSum.resize(timeline.size()); } for (size_t i = 0; i < timeline.size(); i++) { node->TimelineSum[i] += timeline[i]; } if (node == SelectedNode && !timeline.empty()) { node->Slices.emplace_back(); NAnalytics::TRow& row = node->Slices.back(); ui64 prev = 0; for (size_t i = 0; i < timeline.size(); i++) { // Note that col names should go in lexicographical order // in the same way as slices go in pattern timeline double sliceMs = double(timeline[i] - prev) * 1000.0 / NHPTimer::GetClockRate(); row[Sprintf("%09lu", i)] = sliceMs; prev = timeline[i]; } } // Interlink node and track node->Tracks.emplace_back(&track, resTotal, resLast); track.LastNode = node; } bool CheckPattern(const char*& pi, const char* pe, TStringBuf str) { auto si = str.begin(), se = str.end(); for (;pi != pe && si != se; ++pi, ++si) { if (*pi != *si) { return false; } } return si == se; } #define WWW_CHECK_PATTERN(str) if (!CheckPattern(pi, pe, (str))) { return false; } bool MatchTrack(const TTrack& track, const TString& patternStr) { const char* pi = patternStr.data(); const char* pe = pi + patternStr.size(); WWW_CHECK_PATTERN("/"); for (TTrackIter i = TTrackTr::begin(track), e = TTrackTr::end(track); i != e; ++i) { if (pi == pe) { return true; } const NLWTrace::TLogItem& item = *i; WWW_CHECK_PATTERN("/"); WWW_CHECK_PATTERN(item.Probe->Event.GetProvider()); WWW_CHECK_PATTERN("."); WWW_CHECK_PATTERN(item.Probe->Event.Name); while (true) { if (pi == pe) { return true; } char c = *pi; if (c == '/') { break; } else if (c == '@') { pi++; // Parse rollbacks TStringBuf p(pi, pe); size_t dot = p.find('.'); if (dot == TStringBuf::npos) { return false; } size_t rollbacks = 0; try { rollbacks = FromString(p.substr(0, dot)); } catch (...) { return false; } // Parse param name size_t equals = p.find('=', dot + 1); if (equals == TStringBuf::npos) { return false; } TStringBuf paramName = p.substr(dot + 1, equals - dot - 1); pi += equals + 1; // Advance to value // Check param value if ((i64)rollbacks < 0 || std::distance(TTrackTr::begin(track), i) < (i64)rollbacks) { return false; } const NLWTrace::TLogItem& mitem = *(i - rollbacks); if (mitem.SavedParamsCount == 0) { return false; } TString paramValues[LWTRACE_MAX_PARAMS]; TString* paramValue = nullptr; mitem.Probe->Event.Signature.SerializeParams(mitem.Params, paramValues); for (size_t pi = 0; pi < mitem.SavedParamsCount; pi++) { if (mitem.Probe->Event.Signature.ParamNames[pi] == paramName) { paramValue = ¶mValues[pi]; } } if (!paramValue) { return false; } WWW_CHECK_PATTERN(*paramValue); } else { return false; } } } return true; } #undef WWW_CHECK_PATTERN // Push new track through pattern tree void AddTrack(TTrack& track) { // Truncate long tracks if (track.Items.size() > 50) { track.Items.resize(50); } if (SelectedPattern) { if (!MatchTrack(track, SelectedPattern)) { return; } } Timeline.clear(); TPatternNode* node = &Root; AddTrackToNode(node, track, 0, Timeline); ui64 trackStart = TTrackTr::front(track).TimestampCycles; for (TTrackIter i = TTrackTr::begin(track), e = TTrackTr::end(track); i != e;) { // Get or create child by classification TPatternNode* parent = node; node = node->Classifier->Classify(i, track); if (!node->Classifier) { InitNode(node, parent); } const NLWTrace::TLogItem& item = *i; ui64 resTotal = item.TimestampCycles - trackStart; if (i != TTrackTr::begin(track)) { Timeline.push_back(resTotal); } AddTrackToNode(node, track, resTotal, Timeline); // Move through track if (!node->Classifier->KeepHead) { ++i; } } } // Traverse pattern tree (the only way to extract data from it) template void Traverse(TOnNode&& onNode, TOnDescend&& onDescend, TOnAscend&& onAscend) { struct TVisitor : public IVisitor { TOnNode OnNode; TOnDescend OnDescend; TOnAscend OnAscend; TVisitor(TOnNode&& onNode, TOnDescend&& onDescend, TOnAscend&& onAscend) : OnNode(onNode) , OnDescend(onDescend) , OnAscend(onAscend) {} virtual void Visit(TPatternNode* node) override { OnNode(node); if (!node->Classifier->IsLeaf()) { OnDescend(); node->Classifier->Accept(this); OnAscend(); } } }; TVisitor visitor(std::move(onNode), std::move(onDescend), std::move(onAscend)); visitor.Visit(&Root); } TPatternNode* GetRoot() { return &Root; } private: void InitNode(TPatternNode* node, TPatternNode* parent) { node->Parent = parent; auto iter = ParamClassifiers.find(node->GetPath()); if (iter != ParamClassifiers.end()) { node->Classifier.Reset(new TClassifyByParam(node, iter->second.first, iter->second.second)); } else { node->Classifier.Reset(new TClassifyByProbe(node)); } } }; class TLogTrackExtractor: public TLogFilter { private: // Data storage TMultiMap> Items; TVector Depot; // Data refs organized in tracks THashMap Tracks; TVector TracksFromDepot; // Analysis TVector GroupBy; THashSet TrackIds; // The same content as in GroupBy TTimestampCutter CutTs; TPatternTree Tree; public: TLogTrackExtractor(const TCgiParameters& e, const TVector& filters, const TVector& groupBy) : TLogFilter(filters) , CutTs(true) // Always cut input data for tracks , Tree(e) { for (const TString& groupParam : groupBy) { GroupBy.push_back(groupParam); TrackIds.insert(groupParam); } } // For reading lwtrace log (input point for all data) void Push(TThread::TId tid, const NLWTrace::TLogItem& item) { CutTs.Push(tid, item); if (Filter(item)) { Items.emplace(item.TimestampCycles, std::make_pair(tid, item)); } } // For reading lwtrace depot (input point for all data) void Push(TThread::TId, const NLWTrace::TTrackLog& tl) { if (Filter(tl)) { Depot.emplace_back(tl); } } // Analyze logs that have been read void Run() { RunImplLog(); RunImplDepot(); } void RunImplLog() { // Create tracks by filling them with lwtrace items in order of occurance time for (auto& kv : Items) { AddItemToTrack(kv.second.first, kv.second.second); } // Push tracks throught pattern tree for (auto& kv : Tracks) { TTrack& track = kv.second; track.TrackId = kv.first; Tree.AddTrack(track); } } void RunImplDepot() { // Create tracks from depot // OPTIMIZE[serxa]: this convertion is not necessary, done just to keep things simple for (NLWTrace::TTrackLog& tl : Depot) { TTrack& track = TracksFromDepot.emplace_back(); track.TrackId = ToString(tl.Id); for (const NLWTrace::TTrackLog::TItem& i : tl.Items) { track.Items.emplace_back(i.ThreadId, i); } } for (TTrack& t : TracksFromDepot) { Tree.AddTrack(t); } } // Selected node distribution NAnalytics::TTable Distribution(const TString& bn, const TString& b1Str, const TString& b2Str, const TString& widthStr) { using namespace NAnalytics; const NAnalytics::TTable& inputTable = Tree.GetSelectedTable(); double b1 = b1Str? FromString(b1Str): MinValue(bn, inputTable); double b2 = b2Str? FromString(b2Str): MaxValue(bn, inputTable); if (isfinite(b1) && isfinite(b2)) { WWW_CHECK(b1 <= b2, "invalid xrange [%le; %le]", b1, b2); double width = widthStr? FromString(widthStr): 99; double dx = (b2 - b1) / width; if (!(dx > 0)) { dx = 1.0; } return HistogramAll(inputTable, bn, b1, b2, dx); } else { // Empty table -- it's ok -- leave data table empty return NAnalytics::TTable(); } } // Selected sample template void OutputSample(const TString& bn, double b1, double b2, const TSampleOpts& opts, TReader& reader) { Tree.OutputSelectedSample(bn, b1, b2, opts, reader); } // Tabular representation of tracks data void OutputTable(IOutputStream& os, const TCgiParameters& e) { ui64 tracksTotal = Tree.GetRoot()->TrackCount; double maxAvgResTotal = 0; double maxMaxResTotal = 0; Tree.Traverse([&] (TPatternNode* node) { if (node->TrackCount > 0) { maxAvgResTotal = Max(maxAvgResTotal, double(node->ResTotalSum) / node->TrackCount); maxMaxResTotal = Max(maxMaxResTotal, double(node->ResTotalMax)); Sort(node->ResTotalAll); Sort(node->ResLastAll); } }, [&] () { // On descend }, [&] () { // On ascend }); double maxTime = Min(maxMaxResTotal, 1.25 * maxAvgResTotal); double percentile = e.Get("ile")? FromString(e.Get("ile")): 90; WWW_CHECK(percentile >= 0.0 && percentile <= 100.0, "wrong percentile: %lf", percentile); ui64 row = 0; TVector chain; HTML(os) { TABLE_CLASS("tracks-tree") { TABLEHEAD() { os << ""; os << "#"; os << "Pattern"; os << ""; DIV_CLASS("rotate") { os << "Track Count"; } os << ""; os << "Share"; os << "Total, ms"; os << "Last, ms"; os << "Global Timeline"; os << ""; TABLEH() DIV_CLASS("rotate") { os << "Absolute"; } TABLEH() DIV_CLASS("rotate") { os << "Relative"; } TABLEH() DIV_CLASS("rotate") { os << "Average"; } TABLEH() DIV_CLASS("rotate") { os << percentile << "%-ile"; } TABLEH() DIV_CLASS("rotate") { os << "Average"; } TABLEH() DIV_CLASS("rotate") { os << percentile << "%-ile"; } os << ""; } TABLEBODY() { if (tracksTotal == 0) { return; } Tree.Traverse([&] (TPatternNode* node) { TString parentClass; if (!chain.empty()) { parentClass = " treegrid-parent-" + ToString(chain.back()); } TString selectedClass; if (e.Get("pattern") == node->GetPath()) { selectedClass = " danger"; } TABLER_CLASS("treegrid-" + ToString(++row) + parentClass + selectedClass) { // Counting ui64 tracksParent = node->Parent? node->Parent->TrackCount: tracksTotal; double absShare = double(node->TrackCount) * 100 / tracksTotal; double relShare = double(node->TrackCount) * 100 / tracksParent; // Resource total double avgResTotal = double(node->ResTotalSum) / node->TrackCount; size_t ileResTotalIdx = node->ResTotalAll.size() * percentile / 100; if (ileResTotalIdx > 0) { ileResTotalIdx--; } double ileResTotal = double(ileResTotalIdx >= node->ResTotalAll.size()? 0: node->ResTotalAll[ileResTotalIdx]); double avgResTotalMs = avgResTotal * 1000.0 / NHPTimer::GetClockRate(); double ileResTotalMs = ileResTotal * 1000.0 / NHPTimer::GetClockRate(); // Resource last double avgResLast = double(node->ResLastSum) / node->TrackCount; size_t ileResLastIdx = node->ResLastAll.size() * percentile / 100; if (ileResLastIdx > 0) { ileResLastIdx--; } double ileResLast = double(ileResLastIdx >= node->ResLastAll.size()? 0: node->ResLastAll[ileResLastIdx]); double avgResLastMs = avgResLast * 1000.0 / NHPTimer::GetClockRate(); double ileResLastMs = ileResLast * 1000.0 / NHPTimer::GetClockRate(); // Output TABLED() { os << row; } TABLED_CLASS("treegrid-element") { OutputPattern(os, e, node); } TABLED() { os << node->TrackCount; } TABLED() { OutputShare(os, absShare); } TABLED() { OutputShare(os, relShare); } TABLED() { os << FormatFloat(avgResTotalMs); } TABLED() { os << FormatFloat(ileResTotalMs); } TABLED() { os << FormatFloat(avgResLastMs); } TABLED() { os << FormatFloat(ileResLastMs); } TABLED() { OutputTimeline(os, MakeTimeline(node), maxTime); } } }, [&] () { // On descend chain.push_back(row); }, [&] () { // On ascend chain.pop_back(); }); } } } } // Chromium-compatible trace representation of tracks data void OutputChromeTrace(IOutputStream& os, const TCgiParameters& e) { Y_UNUSED(e); TChromeTrace tr; for (TPatternNode::TTrackEntry& entry: Tree.GetRoot()->Tracks) { TTrack* track = entry.Track; auto first = TTrackTr::begin(*track); auto last = TTrackTr::rbegin(*track); TString name = track->LastNode->GetPath(); const NLWTrace::TLogItem& firstItem = *first; TThread::TId firstTid = first->ThreadId; tr.Add(firstTid, firstItem.TimestampCycles, "b", "track", nullptr, name, track->TrackId); for (auto cur = TTrackTr::begin(*track), end = TTrackTr::end(*track); cur != end; ++cur) { const NLWTrace::TLogItem& item = *cur; tr.Add(cur->ThreadId, item.TimestampCycles, "i", "event", &item, GetProbeName(item.Probe)); TString sliceName = GetProbeName(item.Probe); auto next = cur + 1; if (next != end) { const NLWTrace::TLogItem& nextItem = *next; tr.Add(cur->ThreadId, item.TimestampCycles, "b", "track", &item, sliceName, track->TrackId); tr.Add(next->ThreadId, nextItem.TimestampCycles, "e", "track", &nextItem, sliceName, track->TrackId); } else { tr.Add(cur->ThreadId, item.TimestampCycles, "n", "track", &item, sliceName, track->TrackId); } } const NLWTrace::TLogItem& lastItem = *last; tr.Add(last->ThreadId, lastItem.TimestampCycles, "e", "track", nullptr, name, track->TrackId); } tr.Output(os); } void OutputSliceCovarianceMatrix(IOutputStream& os, const TCgiParameters& e) { Y_UNUSED(e); TPatternNode* node = Tree.GetSelectedNode(); if (!node) { return; } NAnalytics::TMatrix covMatrix = NAnalytics::CovarianceMatrix(node->Slices); double var = covMatrix.CellSum(); double covMax = 0.0; for (double x : covMatrix) { if (covMax < x) { covMax = x; } } double dangerCov = covMax * 0.9 * 0.9; double warnCov = covMax * 0.5 * 0.5; HTML(os) { TABLE() { TTimeline timeline = MakeTimeline(node); TABLED() {}; TABLEHEAD() TABLER() { for (auto& e : timeline) TABLED() { TPatternNode* subnode = e.first; os << subnode->Name; } } auto tl = timeline.begin(); TABLEBODY() for (size_t row = 0; row < covMatrix.Rows; row++) TABLER() { TABLEH() { if (tl != timeline.end()) { TPatternNode* subnode = tl->first; os << subnode->Name; ++tl; } } for (size_t col = 0; col < covMatrix.Cols; col++) { double cov = covMatrix.Cell(row, col); TString tdClass = (cov >= dangerCov? "danger": (cov >= warnCov? "warning": "")); TABLED_CLASS(tdClass) { double sigmaX = (covMatrix.Cell(row, row) > 0? sqrt(covMatrix.Cell(row, row)): 0); double sigmaY = (covMatrix.Cell(col, col) > 0? sqrt(covMatrix.Cell(col, col)): 0); os << Sprintf("cov=%.3lf ms2 (%.3lf ms) corr=%.1lf%% var_share=%.1lf%%", cov, sqrt(abs(cov)), cov * 100.0 / sigmaX / sigmaY, cov * 100.0 / var); } } } } } } private: TPatternNode* RollbackFind(TPatternNode* node) { for (;node != nullptr; node = node->Parent) { if (node->Desc.Type == NT_PROBE) { return node; } } return nullptr; } void OutputPattern(IOutputStream& os, const TCgiParameters& e, TPatternNode* node) { // Fill pattern name TString patternName; TString patternTitle; switch (node->Desc.Type) { case NT_ROOT: patternName = "All Tracks"; break; case NT_PROBE: patternTitle = GetProbeName(node->Desc.Probe); patternName = node->Desc.Probe->Event.Name; break; case NT_PARAM: patternName.append(node->Desc.ParamName + " = " + node->Desc.ParamValue); break; } os << "GetPath()}, {"ptrn_anlz", e.Get("ptrn_anlz") ? e.Get("ptrn_anlz") : "resTotal"}, {"linesfill", "y"}, {"linessteps", "y"}, {"pointsshow", "n"}, {"sel_x1", e.Get("sel_x1") ? e.Get("sel_x1") : "0"}, {"sel_x2", e.Get("sel_x2") ? e.Get("sel_x2") : "inf"}}) << "\"" " title=\"" + patternTitle + "\">" << patternName << ""; // Add/remove node menu if (node->Desc.Type != NT_ROOT) { os << "
    "; if (node->Desc.Type == NT_PARAM) { os<< ""; } if (node->Classifier->GetChildType() != NT_PARAM) { os << "" "
      " "
    • Classify by param:
    • "; int rollbacks = 0; TPatternNode* probeNode = node; while (probeNode = RollbackFind(probeNode)) { const NLWTrace::TProbe* probe = probeNode->Desc.Probe; os << "
    • " << GetProbeName(probe) << "
    • "; const NLWTrace::TSignature* sgn = &probe->Event.Signature; for (size_t pi = 0; pi < sgn->ParamCount; pi++) { TString param = sgn->ParamNames[pi]; if (TrackIds.contains(param) || IsFiltered(param)) { continue; } os << "
    • GetPath() + "@" + ToString(rollbacks) + "." + param) << "\">" << param << "
    • "; } rollbacks++; probeNode = probeNode->Parent; } os << "
    "; } os << "
    "; } } void OutputShare(IOutputStream& os, double share) { double lshare = share; double rshare = 100 - lshare; os << "
    " "
    " "
    " "
    " "
    " "" << (share == 100? "100%": Sprintf("%2.1lf%%", share)) << "" "
    "; } using TTimeline = TVector>; TTimeline MakeTimeline(TPatternNode* node) { TTimeline ret; if (node->TrackCount == 0) { return ret; } ret.reserve(node->TimelineSum.size()); for (double time : node->TimelineSum) { ret.emplace_back(nullptr, double(time) / node->TrackCount); } TPatternNode* n = node; for (auto i = ret.rbegin(), e = ret.rend(); i != e; ++i) { WWW_CHECK(n, "internal bug: wrong timeline length at pattern node '%s'", node->GetPath().data()); i->first = n; n = n->Parent; } return ret; } void OutputTimeline(IOutputStream& os, const TTimeline& timeline, double maxTime) { static const char *barClass[] = { "progress-bar-info", "progress-bar-warning" }; if (timeline.empty()) { return; } os << "
    "; double prevPos = 0.0; double prevTime = 0.0; size_t i = 0; for (auto& e : timeline) { TPatternNode* node = e.first; double time = e.second; double pos = time * 100 / maxTime; if (pos > 100) { pos = 100; } double width = pos - prevPos; os << "
    "; if (width > 20) { // To ensure text will fit the bar os << FormatCycles(time - prevTime); } os << "
    "; prevPos = pos; prevTime = time; i++; } os << "
    "; } TString FormatTimelineTooltip(double time, double prevTime, TPatternNode* node) { return FormatCycles(time - prevTime) + ": " + FormatCycles(prevTime) + " -> " + FormatCycles(time) + "(" + node->Name + ")"; } TString FormatFloat(double value) { if (value == 0.0) { return "0"; } if (value > 1.0) { if (value > 100.0) { return Sprintf("%.0lf", value); } if (value > 10.0) { return Sprintf("%.1lf", value); } return Sprintf("%.2lf", value); } else if (value > 1e-3) { if (value > 1e-1) { return Sprintf("%.3lf", value); } if (value > 1e-2) { return Sprintf("%.4lf", value); } return Sprintf("%.5lf", value); } else if (value > 1e-6) { if (value > 1e-4) { return Sprintf("%.6lf", value); } if (value > 1e-5) { return Sprintf("%.7lf", value); } return Sprintf("%.8lfus", value); } else { if (value > 1e-7) { return Sprintf("%.9lfns", value); } if (value > 1e-8) { return Sprintf("%.10lfns", value); } return Sprintf("%.2le", value); } } TString FormatCycles(double timeCycles) { double timeSec = timeCycles / NHPTimer::GetClockRate(); if (timeSec > 1.0) { if (timeSec > 100.0) { return Sprintf("%.0lfs", timeSec); } if (timeSec > 10.0) { return Sprintf("%.1lfs", timeSec); } return Sprintf("%.2lfs", timeSec); } else if (timeSec > 1e-3) { if (timeSec > 1e-1) { return Sprintf("%.0lfms", timeSec * 1e3); } if (timeSec > 1e-2) { return Sprintf("%.1lfms", timeSec * 1e3); } return Sprintf("%.2lfms", timeSec * 1e3); } else if (timeSec > 1e-6) { if (timeSec > 1e-4) { return Sprintf("%.0lfus", timeSec * 1e6); } if (timeSec > 1e-5) { return Sprintf("%.1lfus", timeSec * 1e6); } return Sprintf("%.2lfus", timeSec * 1e6); } else { if (timeSec > 1e-7) { return Sprintf("%.0lfns", timeSec * 1e9); } if (timeSec > 1e-8) { return Sprintf("%.1lfns", timeSec * 1e9); } return Sprintf("%.2lfns", timeSec * 1e9); } } TString GetParam(const NLWTrace::TLogItem& item, TString* paramValues, const TString& paramName) { for (size_t pi = 0; pi < item.SavedParamsCount; pi++) { if (paramName == item.Probe->Event.Signature.ParamNames[pi]) { return paramValues[pi]; } } return TString(); } TString GetGroup(const NLWTrace::TLogItem& item, TString* paramValues) { TStringStream ss; bool first = true; for (const TString& groupParam : GroupBy) { ss << (first? "": "|") << GetParam(item, paramValues, groupParam); first = false; } return ss.Str(); } void AddItemToTrack(TThread::TId tid, const NLWTrace::TLogItem& item) { // Ensure cyclic per thread lwtrace logs wont drop *inner* items of a track // (note that some *starting* items can be dropped) if (item.SavedParamsCount > 0 && !CutTs.Skip(item)) { TString paramValues[LWTRACE_MAX_PARAMS]; item.Probe->Event.Signature.SerializeParams(item.Params, paramValues); Tracks[GetGroup(item, paramValues)].Items.emplace_back(tid, item); } } }; NLWTrace::TProbeRegistry g_Probes; TString g_sanitizerTest("TString g_sanitizerTest"); NLWTrace::TManager g_SafeManager(g_Probes, false); NLWTrace::TManager g_UnsafeManager(g_Probes, true); TDashboardRegistry g_DashboardRegistry; class TLWTraceMonPage : public NMonitoring::IMonPage { private: NLWTrace::TManager* TraceMngr; TString StartTime; TTraceCleaner Cleaner; TMutex SnapshotsMtx; THashMap> Snapshots; public: explicit TLWTraceMonPage(bool allowUnsafe = false) : NMonitoring::IMonPage("trace", "Tracing") , TraceMngr(&TraceManager(allowUnsafe)) , Cleaner(TraceMngr) { time_t stime = TInstant::Now().TimeT(); StartTime = CTimeR(&stime); } virtual void Output(NMonitoring::IMonHttpRequest& request) { TStringStream out; try { if (request.GetParams().Get("mode") == "") { OutputTracesAndSnapshots(request, out); } else if (request.GetParams().Get("mode") == "probes") { OutputProbes(request, out); } else if (request.GetParams().Get("mode") == "dashboards") { OutputDashboards(request, out); } else if (request.GetParams().Get("mode") == "dashboard") { OutputDashboard(request, out); } else if (request.GetParams().Get("mode") == "log") { OutputLog(request, out); } else if (request.GetParams().Get("mode") == "query") { OutputQuery(request, out); } else if (request.GetParams().Get("mode") == "builder") { OutputBuilder(request, out); } else if (request.GetParams().Get("mode") == "analytics") { OutputAnalytics(request, out); } else if (request.GetParams().Get("mode") == "new") { PostNew(request, out); } else if (request.GetParams().Get("mode") == "delete") { PostDelete(request, out); } else if (request.GetParams().Get("mode") == "make_snapshot") { PostSnapshot(request, out); } else if (request.GetParams().Get("mode") == "settimeout") { PostSetTimeout(request, out); } else { ythrow yexception() << "Bad request"; } } catch (TPageGenBase& gen) { out.Clear(); out << gen.what(); } catch (...) { out.Clear(); if (request.GetParams().Get("error") == "text") { // Text error reply is helpful for ajax requests out << NMonitoring::HTTPOKTEXT; out << CurrentExceptionMessage(); } else { WWW_HTML(out) { out << "

    Error

    "
                            << CurrentExceptionMessage()
                            << Endl;
                    }
                }
            }
            request.Output() << out.Str();
        }
    
    private:
        void OutputNavbar(const NMonitoring::IMonHttpRequest& request, IOutputStream& out)
        {
            TString active = " class=\"active\"";
            out <<
                ""
                ;
        }
    
        template 
        void ReadSnapshots(TReader& reader) const
        {
            TGuard g(SnapshotsMtx);
            for (const auto& kv : Snapshots) {
                reader.Push(kv.first, kv.second);
            }
        }
    
        void OutputTracesAndSnapshots(const NMonitoring::IMonHttpRequest& request, IOutputStream& out)
        {
            TLogSources logSources(Cleaner);
            TraceMngr->ReadTraces(logSources);
            ReadSnapshots(logSources);
    
            TStringStream ss;
            TTracesHtmlPrinter printer(ss);
            logSources.ForEach(printer);
            WWW_HTML(out) {
                OutputNavbar(request, out);
                out <<
                    ""
                    ""
                    << ss.Str() <<
                    "
    Start TimeTimeoutNameEventsThreads
    " ; out << "

    Start time: " << StartTime; out << "
    Build date: "; out << __DATE__ << " " << __TIME__ << "

    " << Endl; } } void OutputProbes(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) { TStringStream ss; TProbesHtmlPrinter printer; TraceMngr->ReadProbes(printer); printer.Output(ss); WWW_HTML(out) { OutputNavbar(request, out); out << ss.Str(); } } void OutputDashboards(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) { TStringStream ss; g_DashboardRegistry.Output(ss); WWW_HTML(out) { OutputNavbar(request, out); out << ss.Str(); } } void OutputDashboard(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) { if (!request.GetParams().Has("name")) { ythrow yexception() << "Cgi-parameter 'name' is not specified"; } else { auto name = request.GetParams().Get("name"); NLWTrace::TDashboard dash; if (!g_DashboardRegistry.Get(name, dash)) { ythrow yexception() << "Dashboard doesn't exist"; } WWW_HTML(out) { OutputNavbar(request, out); out << ""; out << "

    " << dash.GetName() << "

    "; if (dash.GetDescription()) { out << "

    " << dash.GetDescription() << "

    "; } int height = 85; // % int minHeight = 100; // px out << ""; ui32 rows = 0; auto maxRowSpan = [](const auto& row) { ui32 rowSpan = 1; for (const auto& cell : row.GetCells()) { rowSpan = Max(rowSpan, cell.GetRowSpan()); } return rowSpan; }; for (const auto& row : dash.GetRows()) { rows += maxRowSpan(row); } for (const auto& row : dash.GetRows()) { int rowSpan = maxRowSpan(row); out << ""; for (const auto& cell : row.GetCells()) { TString url = cell.GetUrl(); TString title = cell.GetTitle(); TString text = cell.GetText(); auto rowSpan = Max(1, cell.GetRowSpan()); auto colSpan = Max(1, cell.GetColSpan()); if (url) { if (title) { out << ""; // Add fake cells to fix html table for (ui32 left = 1; left < colSpan; ++left) { out << ""; } } else { out << ""; } } } out << "
    " << title << "
    "; } out << "
    " << "" << text << "
    "; } } } static double ParseDouble(const TString& s) { if (s == "inf") { return std::numeric_limits::infinity(); } else if (s == "-inf") { return -std::numeric_limits::infinity(); } else { return FromString(s); } } void OutputLog(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) { if (request.GetParams().NumOfValues("id") == 0) { ythrow yexception() << "Cgi-parameter 'id' is not specified"; } else { const TCgiParameters& e = request.GetParams(); TStringStream ss; if (e.Get("format") == "json") { TLogJsonPrinter printer(ss); printer.OutputHeader(); TString id = e.Get("id"); CheckAdHocTrace(id, TDuration::Minutes(1)); TraceMngr->ReadLog(id, printer); printer.OutputFooter(TraceMngr->GetTrace(id)); out << HTTPOKJSON; out << ss.Str(); } if (e.Get("format") == "json2") { TLogTextPrinter printer(e); for (const TString& id : Subvalues(e, "id")) { CheckAdHocTrace(id, TDuration::Minutes(1)); TraceMngr->ReadLog(id, printer); TraceMngr->ReadDepot(id, printer); } printer.OutputJson(ss); out << HTTPOKJSON; out << ss.Str(); } else if (e.Get("format") == "analytics" && e.Get("aggr") == "tracks") { TLogTrackExtractor logTrackExtractor(e, Subvalues(e, "f"), Subvalues(e, "g") ); for (const TString& id : Subvalues(e, "id")) { CheckAdHocTrace(id, TDuration::Minutes(1)); TraceMngr->ReadLog(id, logTrackExtractor); TraceMngr->ReadDepot(id, logTrackExtractor); } TString patternAnalyzer; if (e.Get("pattern")) { patternAnalyzer = e.Get("ptrn_anlz"); } logTrackExtractor.Run(); TLogTextPrinter printer(e); const TString& distBy = patternAnalyzer; double sel_x1 = e.Get("sel_x1")? ParseDouble(e.Get("sel_x1")): NAN; double sel_x2 = e.Get("sel_x2")? ParseDouble(e.Get("sel_x2")): NAN; TSampleOpts opts; opts.ShowProvider = (e.Get("show_provider") == "y"); if (e.Get("size_limit")) { opts.SizeLimit = FromString(e.Get("size_limit")); } logTrackExtractor.OutputSample(distBy, sel_x1, sel_x2, opts, printer); printer.Output(ss); out << HTTPOKTEXT; out << ss.Str(); } else { TLogTextPrinter printer(e); for (const TString& id : Subvalues(e, "id")) { CheckAdHocTrace(id, TDuration::Minutes(1)); TraceMngr->ReadLog(id, printer); TraceMngr->ReadDepot(id, printer); } printer.Output(ss); out << HTTPOKTEXT; out << ss.Str(); } } } void OutputQuery(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) { if (request.GetParams().NumOfValues("id") == 0) { ythrow yexception() << "Cgi-parameter 'id' is not specified"; } else { TString id = request.GetParams().Get("id"); const NLWTrace::TQuery& query = TraceMngr->GetTrace(id)->GetQuery(); TString queryStr = query.DebugString(); WWW_HTML(out) { out << "

    Trace Query: " << id << "

    " << queryStr;
                }
            }
        }
    
        void OutputBuilder(const NMonitoring::IMonHttpRequest& request, IOutputStream& out)
        {
            Y_UNUSED(request);
            WWW_HTML(out) {
                OutputNavbar(request, out);
                out << "
    "; DIV_CLASS("form-group") { LABEL_CLASS_FOR("col-sm-1 control-label", "inputId") { out << "Name"; } DIV_CLASS("col-sm-11") { out << ""; } } DIV_CLASS("form-group") { LABEL_CLASS_FOR("col-sm-1 control-label", "textareaQuery") { out << "Query"; } DIV_CLASS("col-sm-11") { out << ""; } } DIV_CLASS("form-group") { DIV_CLASS("col-sm-offset-1 col-sm-11") { out << ""; } } out << "
    "; } } void OutputAnalytics(const NMonitoring::IMonHttpRequest& request, TStringStream& out) { using namespace NAnalytics; const TCgiParameters& e = request.GetParams(); TLogSources logSources(Cleaner); TraceMngr->ReadTraces(logSources); ReadSnapshots(logSources); RequireMultipleSelection(out, e, "id", "Analyze ", ListTraces(logSources)); THolder logFilter; TLogAnalyzer* logAnalyzer = nullptr; TLogTrackExtractor* logTracks = nullptr; if (request.GetParams().Get("aggr") == "tracks") { logFilter.Reset(logTracks = new TLogTrackExtractor(e, Subvalues(request.GetParams(), "f"), Subvalues(request.GetParams(), "g") )); for (const TString& id : Subvalues(request.GetParams(), "id")) { CheckAdHocTrace(id, TDuration::Minutes(1)); TraceMngr->ReadLog(id, *logTracks); TraceMngr->ReadDepot(id, *logTracks); } } else { logFilter.Reset(logAnalyzer = new TLogAnalyzer( Subvalues(request.GetParams(), "f"), Subvalues(request.GetParams(), "g"), request.GetParams().Get("cutts") == "y" )); for (const TString& id : Subvalues(request.GetParams(), "id")) { CheckAdHocTrace(id, TDuration::Minutes(1)); TraceMngr->ReadLog(id, *logAnalyzer); TraceMngr->ReadDepot(id, *logAnalyzer); } } logFilter->FilterSelectors(out, e, "f"); OptionalMultipleSelection(out, e, "g", "group by", logFilter->ListParamNames()); { auto paramNamesList = logFilter->ListParamNames(); if (e.Get("aggr") == "tracks") { paramNamesList.emplace_back("_trackMs", "_trackMs"); } OptionalSelection(out, e, "s", "order by", paramNamesList); } if (e.Get("s")) { TVariants variants; variants.emplace_back("", "asc"); variants.emplace_back("y", "desc"); DropdownSelector(out, e, "reverse", e.Get("reverse"), "", variants); } TString aggr = e.Get("aggr"); TVariants variants1; // MSVS2013 doesn't understand complex initializer lists variants1.emplace_back("", "without aggregation"); variants1.emplace_back("hist", "as histogram"); variants1.emplace_back("tracks", "tracks"); DropdownSelector(out, e, "aggr", e.Get("aggr"), "", variants1); unsigned refresh = e.Get("refresh")? FromString(e.Get("refresh")): 1000; if (aggr == "tracks") { TVariants ileVars; ileVars.emplace_back("0", "0"); ileVars.emplace_back("25", "25"); ileVars.emplace_back("50", "50"); ileVars.emplace_back("75", "75"); ileVars.emplace_back("", "90"); ileVars.emplace_back("95", "95"); ileVars.emplace_back("99", "99"); ileVars.emplace_back("99.9", "99.9"); ileVars.emplace_back("100", "100"); DropdownSelector(out, e, "ile", e.Get("ile"), "and show", ileVars); out << "%-ile. "; TString patternAnalyzer; TString distBy; TString distType; if (e.Get("pattern")) { TVariants analyzePatternVars; analyzePatternVars.emplace_back("resTotal", "distribution by total"); analyzePatternVars.emplace_back("resLast", "distribution by last"); analyzePatternVars.emplace_back("covMatrix", "covariance matrix"); DropdownSelector( out, e, "ptrn_anlz", e.Get("ptrn_anlz"), "Pattern", analyzePatternVars ); patternAnalyzer = e.Get("ptrn_anlz"); TVariants distTypeVars; distTypeVars.emplace_back("", "as is"); distTypeVars.emplace_back("-stack", "cumulative"); DropdownSelector(out, e, "dist_type", e.Get("dist_type"), "", distTypeVars); distType = e.Get("dist_type"); } else { out << "Select pattern for more options"; } logTracks->Run(); if (e.Get("download") == "y") { out.Clear(); out << "HTTP/1.1 200 Ok\r\n" "Content-Type: application/force-download\r\n" "Content-Transfer-Encoding: binary\r\n" "Content-Disposition: attachment; filename=\"trace_chrome.json\"\r\n" "\r\n" ; logTracks->OutputChromeTrace(out, e); return; } NAnalytics::TTable distData; bool showSample = false; TLogTextPrinter printer(e); if (patternAnalyzer == "resTotal" || patternAnalyzer == "resLast") { distBy = patternAnalyzer; distData = logTracks->Distribution(distBy, "", "", e.Get("width")); double sel_x1 = e.Get("sel_x1")? ParseDouble(e.Get("sel_x1")): NAN; double sel_x2 = e.Get("sel_x2")? ParseDouble(e.Get("sel_x2")): NAN; if (!isnan(sel_x1) && !isnan(sel_x2)) { showSample = true; TSampleOpts opts; opts.ShowProvider = (e.Get("show_provider") == "y"); if (e.Get("size_limit")) { opts.SizeLimit = FromString(e.Get("size_limit")); } logTracks->OutputSample(distBy, sel_x1, sel_x2, opts, printer); } } TString selectors = out.Str(); out.Clear(); out << NMonitoring::HTTPOKHTML; out << "" << Endl; HTML(out) { HTML_TAG() { HEAD() { out << NResource::Find("lwtrace/mon/static/analytics.header.html") << Endl; if (distBy) { out << "\n"; } // Show download button out << "\n"; } BODY() { // Wrap selectors with navbar { TSelectorsContainer sc(out); out << selectors; } logTracks->OutputTable(out, e); if (distBy) { out << NResource::Find("lwtrace/mon/static/analytics.flot.html") << Endl; if (showSample) { static const THashSet keepParams = { "f", "g", "head", "tail", "s", "reverse", "cutts", "showts", "show_provider", "size_limit", "aggr", "id", "pattern", "ptrn_anlz", "sel_x1", "sel_x2" }; TCgiParameters cgiParams; for (const auto& kv : request.GetParams()) { if (keepParams.count(kv.first)) { cgiParams.insert(kv); } } cgiParams.insert(std::pair("mode", "log")); BtnHref(out, "Open logs", MakeUrlAdd(cgiParams, "format", "analytics")); out << "
    \n";
                                    printer.Output(out);
                                    out << "
    \n"; } } if (patternAnalyzer == "covMatrix") { logTracks->OutputSliceCovarianceMatrix(out, e); } } } } } else { double width = e.Get("width")? FromString(e.Get("width")): 99; NAnalytics::TTable data; if (aggr == "") { data = logAnalyzer->GetTable(); } else if (aggr == "hist") { RequireSelection(out, e, "bn", "by", logFilter->ListParamNames()); const NAnalytics::TTable& inputTable = logAnalyzer->GetTable(); TString bn = e.Get("bn"); double b1 = e.Get("b1")? FromString(e.Get("b1")): MinValue(bn, inputTable); double b2 = e.Get("b2")? FromString(e.Get("b2")): MaxValue(bn, inputTable); if (isfinite(b1) && isfinite(b2)) { WWW_CHECK(b1 <= b2, "invalid xrange [%le; %le]", b1, b2); double dx = e.Get("dx")? FromString(e.Get("dx")): (b2-b1)/width; data = HistogramAll(inputTable, e.Get("bn"), b1, b2, dx); } else { // Empty table -- it's ok -- leave data table empty } } TString xn = e.Get("xn"); TString outFormat = e.Get("out"); TVariants variants2; variants2.emplace_back("html", "table"); variants2.emplace_back("flot", "chart"); variants2.emplace_back("gantt", "gantt"); variants2.emplace_back("text", "text"); variants2.emplace_back("csv", "CSV"); variants2.emplace_back("json_flot", "JSON"); RequireSelection(out, e, "out", "and show", variants2); if (outFormat == "csv") { TString sep = e.Get("sep")? e.Get("sep"): TString("\t"); out.Clear(); out << NMonitoring::HTTPOKTEXT; out << ToCsv(data, sep, e.Get("head") != "n"); } else if (outFormat == "html") { TString selectors = out.Str(); out.Clear(); WWW_HTML(out) { // Wrap selectors with navbar { TSelectorsContainer sc(out); out << selectors; } out << ToHtml(data); } } else if (outFormat == "json_flot") { SeriesSelectors(out, e, "xn", "yns", data); out.Clear(); out << NMonitoring::HTTPOKJSON; out << ToJsonFlot(data, xn, SplitString(e.Get("yns"), ":")); } else if (outFormat == "flot") { SeriesSelectors(out, e, "xn", "yns", data); TString selectors = out.Str(); TVector ynos = SplitString(e.Get("yns"), ":"); out.Clear(); out << NMonitoring::HTTPOKHTML; out << "" << Endl; HTML(out) { HTML_TAG() { HEAD() { out << NResource::Find("lwtrace/mon/static/analytics.header.html") << Endl; out << "\n" ; } BODY() { // Wrap selectors with navbar { TSelectorsContainer sc(out); out << selectors; } out << NResource::Find("lwtrace/mon/static/analytics.flot.html") << Endl; } } } } else if (outFormat == "gantt") { TString selectors = out.Str(); out.Clear(); out << NMonitoring::HTTPOKHTML; out << "" << Endl; HTML(out) { HTML_TAG() { HEAD() { out << NResource::Find("lwtrace/mon/static/analytics.header.html") << Endl; out << "\n" ; } BODY() { // Wrap selectors with navbar { TSelectorsContainer sc(out); out << selectors; } out << NResource::Find("lwtrace/mon/static/analytics.gantt.html") << Endl; } } } } else if (outFormat = "text") { out << " " << Endl; out << R"END()END"; TString selectors = out.Str(); TLogTextPrinter printer(e); TStringStream ss; for (const TString& id : Subvalues(e, "id")) { CheckAdHocTrace(id, TDuration::Minutes(1)); TraceMngr->ReadLog(id, printer); TraceMngr->ReadDepot(id, printer); } printer.Output(ss); out.Clear(); out << NMonitoring::HTTPOKHTML; out << "" << Endl; HTML(out) { HTML_TAG() { HEAD() { out << NResource::Find("lwtrace/mon/static/analytics.header.html") << Endl; } BODY() { // Wrap selectors with navbar { TSelectorsContainer sc(out); out << selectors; } static const THashSet keepParams = { "s", "head", "reverse", "cutts", "showts", "id", "out" }; TCgiParameters cgiParams; for (const auto& kv : request.GetParams()) { if (keepParams.count(kv.first)) { cgiParams.insert(kv); } } cgiParams.insert(std::pair("mode", "analytics")); auto toggledButton = [&out, &e, &cgiParams] (const TString& label, const TString& cgiKey) { if (e.Get(cgiKey) == "y") { BtnHref(out, label, MakeUrlErase(cgiParams, cgiKey, "y"), true); } else { BtnHref(out, label, MakeUrlAdd(cgiParams, cgiKey, "y")); } }; toggledButton("Cut Tails", "cutts"); toggledButton("Relative Time", "showts"); cgiParams.erase("mode"); cgiParams.insert(std::pair("mode", "log")); BtnHref(out, "Fullscreen", MakeUrlAdd(cgiParams, "format", "text")); out << "
    \n";
                                out << ss.Str() << Endl;
                                out << "
    \n"; } } } } } } TDuration GetGetTimeout(const NMonitoring::IMonHttpRequest& request) { return (request.GetParams().Has("timeout")? TDuration::Seconds(FromString(request.GetParams().Get("timeout"))): TDuration::Max()); } void PostNew(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) { WWW_CHECK(request.GetPostParams().Has("id"), "POST parameter 'id' is not specified"); const TString& id = request.GetPostParams().Get("id"); bool ui = (request.GetParams().Get("ui") == "y"); TDuration timeout = GetGetTimeout(request); if (!CheckAdHocTrace(id, timeout)) { NLWTrace::TQuery query; TString queryStr = request.GetPostParams().Get("query"); if (!ui) { queryStr = Base64Decode(queryStr); // Needed for trace.sh (historically) } WWW_CHECK(queryStr, "Empty trace query"); bool parsed = NProtoBuf::TextFormat::ParseFromString(queryStr, &query); WWW_CHECK(parsed, "Trace query text protobuf parse failed"); // TODO[serxa]: report error line/col and message TraceMngr->New(id, query); Cleaner.Postpone(id, timeout, false); } else { WWW_CHECK(!request.GetPostParams().Has("query"), "trace id '%s' is reserved for ad-hoc traces", id.data()); } if (ui) { WWW_HTML(out) { out << "
    " "

    Trace created successfully

    " "
    " "\n"; } } else { out << HTTPOKTEXT; out << "OK\n"; } } void PostDelete(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) { WWW_CHECK(request.GetPostParams().Has("id"), "POST parameter 'id' is not specified"); const TString& id = request.GetPostParams().Get("id"); bool ui = (request.GetParams().Get("ui") == "y"); TraceMngr->Delete(id); Cleaner.Forget(id); if (ui) { WWW_HTML(out) { out << "
    " "

    Trace deleted successfully

    " "
    " "\n"; } } else { out << HTTPOKTEXT; out << "OK\n"; } } void PostSnapshot(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) { WWW_CHECK(request.GetPostParams().Has("id"), "POST parameter 'id' is not specified"); const TString& id = request.GetPostParams().Get("id"); bool ui = (request.GetParams().Get("ui") == "y"); TInstant now = TInstant::Now(); TGuard g(SnapshotsMtx); const NLWTrace::TSession* trace = TraceMngr->GetTrace(id); struct tm tm0; TString sid = id + Strftime("_%Y%m%d-%H%M%S", now.GmTime(&tm0)); TAtomicSharedPtr& pbPtr = Snapshots[sid]; pbPtr.Reset(new NLWTrace::TLogPb()); trace->ToProtobuf(*pbPtr); pbPtr->SetName(sid); if (ui) { WWW_HTML(out) { out << "
    " "

    Snapshot created successfully

    " "
    " "\n"; } } else { out << HTTPOKTEXT; out << "OK\n"; } } void PostSetTimeout(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) { WWW_CHECK(request.GetPostParams().Has("id"), "POST parameter 'id' is not specified"); const TString& id = request.GetPostParams().Get("id"); TDuration timeout = GetGetTimeout(request); bool ui = (request.GetParams().Get("ui") == "y"); Cleaner.Postpone(id, timeout, true); if (ui) { WWW_HTML(out) { out << "
    " "

    Timeout changed successfully

    " "
    " "\n"; } } else { out << HTTPOKTEXT; out << "OK\n"; } } void RegisterDashboard(const TString& dashConfig) { g_DashboardRegistry.Register(dashConfig); } private: // Returns true iff trace is ad-hoc and ensures trace is created bool CheckAdHocTrace(const TString& id, TDuration timeout) { TAdHocTraceConfig cfg; if (cfg.ParseId(id)) { if (!TraceMngr->HasTrace(id)) { TraceMngr->New(id, cfg.Query()); } Cleaner.Postpone(id, timeout, false); return true; } return false; } }; void RegisterPages(NMonitoring::TIndexMonPage* index, bool allowUnsafe) { THolder p = MakeHolder(allowUnsafe); index->Register(p.Release()); #define WWW_STATIC_FILE(file, type) \ index->Register(new TResourceMonPage(file, file, NMonitoring::TResourceMonPage::type)); WWW_STATIC_FILE("lwtrace/mon/static/common.css", CSS); WWW_STATIC_FILE("lwtrace/mon/static/common.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/css/bootstrap.min.css", CSS); WWW_STATIC_FILE("lwtrace/mon/static/css/d3-gantt.css", CSS); WWW_STATIC_FILE("lwtrace/mon/static/css/jquery.treegrid.css", CSS); WWW_STATIC_FILE("lwtrace/mon/static/analytics.css", CSS); WWW_STATIC_FILE("lwtrace/mon/static/fonts/glyphicons-halflings-regular.eot", FONT_EOT); WWW_STATIC_FILE("lwtrace/mon/static/fonts/glyphicons-halflings-regular.svg", SVG); WWW_STATIC_FILE("lwtrace/mon/static/fonts/glyphicons-halflings-regular.ttf", FONT_TTF); WWW_STATIC_FILE("lwtrace/mon/static/fonts/glyphicons-halflings-regular.woff2", FONT_WOFF2); WWW_STATIC_FILE("lwtrace/mon/static/fonts/glyphicons-halflings-regular.woff", FONT_WOFF); WWW_STATIC_FILE("lwtrace/mon/static/img/collapse.png", PNG); WWW_STATIC_FILE("lwtrace/mon/static/img/expand.png", PNG); WWW_STATIC_FILE("lwtrace/mon/static/img/file.png", PNG); WWW_STATIC_FILE("lwtrace/mon/static/img/folder.png", PNG); WWW_STATIC_FILE("lwtrace/mon/static/js/bootstrap.min.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/d3.v4.min.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/d3-gantt.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/d3-tip-0.8.0-alpha.1.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/filesaver.min.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/jquery.flot.extents.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/jquery.flot.min.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/jquery.flot.navigate.min.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/jquery.flot.selection.min.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/jquery.min.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/jquery.treegrid.bootstrap3.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/jquery.treegrid.min.js", JAVASCRIPT); WWW_STATIC_FILE("lwtrace/mon/static/js/jquery.url.min.js", JAVASCRIPT); #undef WWW_STATIC_FILE } NLWTrace::TProbeRegistry& ProbeRegistry() { return g_Probes; } NLWTrace::TManager& TraceManager(bool allowUnsafe) { return allowUnsafe? g_UnsafeManager: g_SafeManager; } TDashboardRegistry& DashboardRegistry() { return g_DashboardRegistry; } } // namespace NLwTraceMonPage