| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| // Date: 2014/09/22 19:04:47 |
| |
| #include <pthread.h> |
| #include <set> // std::set |
| #include <fstream> // std::ifstream |
| #include <sstream> // std::ostringstream |
| #include <gflags/gflags.h> |
| #include "butil/macros.h" // BAIDU_CASSERT |
| #include "butil/containers/flat_map.h" // butil::FlatMap |
| #include "butil/scoped_lock.h" // BAIDU_SCOPE_LOCK |
| #include "butil/string_splitter.h" // butil::StringSplitter |
| #include "butil/errno.h" // berror |
| #include "butil/time.h" // milliseconds_from_now |
| #include "butil/file_util.h" // butil::FilePath |
| #include "butil/threading/platform_thread.h" |
| #include "butil/reloadable_flags.h" |
| #include "bvar/gflag.h" |
| #include "bvar/variable.h" |
| #include "bvar/mvariable.h" |
| |
| namespace bvar { |
| |
| DEFINE_bool(save_series, true, |
| "Save values of last 60 seconds, last 60 minutes," |
| " last 24 hours and last 30 days for plotting"); |
| |
| DEFINE_bool(quote_vector, true, |
| "Quote description of Vector<> to make it valid to noah"); |
| |
| // Remember abort request before bvar_abort_on_same_name is initialized. |
| bool s_bvar_may_abort = false; |
| static bool validate_bvar_abort_on_same_name(const char*, bool v) { |
| RELEASE_ASSERT_VERBOSE(!v || !s_bvar_may_abort, "Abort due to name conflict"); |
| return true; |
| } |
| DEFINE_bool(bvar_abort_on_same_name, false, "Abort when names of bvar are same"); |
| BUTIL_VALIDATE_GFLAG(bvar_abort_on_same_name, validate_bvar_abort_on_same_name); |
| |
| |
| DEFINE_bool(bvar_log_dumpped, false, |
| "[For debugging] print dumpped info" |
| " into logstream before call Dumpper"); |
| BUTIL_VALIDATE_GFLAG(bvar_log_dumpped, butil::PassValidate); |
| |
| const size_t SUB_MAP_COUNT = 32; // must be power of 2 |
| BAIDU_CASSERT(!(SUB_MAP_COUNT & (SUB_MAP_COUNT - 1)), must_be_power_of_2); |
| |
| class VarEntry { |
| public: |
| VarEntry() : var(NULL), display_filter(DISPLAY_ON_ALL) {} |
| |
| Variable* var; |
| DisplayFilter display_filter; |
| }; |
| |
| typedef butil::FlatMap<std::string, VarEntry> VarMap; |
| |
| struct VarMapWithLock : public VarMap { |
| pthread_mutex_t mutex; |
| |
| VarMapWithLock() { |
| if (init(1024) != 0) { |
| LOG(WARNING) << "Fail to init VarMap"; |
| } |
| |
| pthread_mutexattr_t attr; |
| pthread_mutexattr_init(&attr); |
| pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); |
| pthread_mutex_init(&mutex, &attr); |
| pthread_mutexattr_destroy(&attr); |
| } |
| }; |
| |
| // We have to initialize global map on need because bvar is possibly used |
| // before main(). |
| static pthread_once_t s_var_maps_once = PTHREAD_ONCE_INIT; |
| static VarMapWithLock* s_var_maps = NULL; |
| |
| static void init_var_maps() { |
| // It's probably slow to initialize all sub maps, but rpc often expose |
| // variables before user. So this should not be an issue to users. |
| s_var_maps = new VarMapWithLock[SUB_MAP_COUNT]; |
| } |
| |
| inline size_t sub_map_index(const std::string& str) { |
| if (str.empty()) { |
| return 0; |
| } |
| size_t h = 0; |
| // we're assume that str is ended with '\0', which may not be in general |
| for (const char* p = str.c_str(); *p; ++p) { |
| h = h * 5 + *p; |
| } |
| return h & (SUB_MAP_COUNT - 1); |
| } |
| |
| inline VarMapWithLock* get_var_maps() { |
| pthread_once(&s_var_maps_once, init_var_maps); |
| return s_var_maps; |
| } |
| |
| inline VarMapWithLock& get_var_map(const std::string& name) { |
| VarMapWithLock& m = get_var_maps()[sub_map_index(name)]; |
| return m; |
| } |
| |
| Variable::~Variable() { |
| CHECK(!hide()) << "Subclass of Variable MUST call hide() manually in their" |
| " dtors to avoid displaying a variable that is just destructing"; |
| } |
| |
| int Variable::expose_impl(const butil::StringPiece& prefix, |
| const butil::StringPiece& name, |
| DisplayFilter display_filter) { |
| if (name.empty()) { |
| LOG(ERROR) << "Parameter[name] is empty"; |
| return -1; |
| } |
| // NOTE: It's impossible to atomically erase from a submap and insert into |
| // another submap without a global lock. When the to-be-exposed name |
| // already exists, there's a chance that we can't insert back previous |
| // name. But it should be fine generally because users are unlikely to |
| // expose a variable more than once and calls to expose() are unlikely |
| // to contend heavily. |
| |
| // remove previous pointer from the map if needed. |
| hide(); |
| |
| // Build the name. |
| _name.clear(); |
| _name.reserve((prefix.size() + name.size()) * 5 / 4); |
| if (!prefix.empty()) { |
| to_underscored_name(&_name, prefix); |
| if (!_name.empty() && butil::back_char(_name) != '_') { |
| _name.push_back('_'); |
| } |
| } |
| to_underscored_name(&_name, name); |
| |
| VarMapWithLock& m = get_var_map(_name); |
| { |
| BAIDU_SCOPED_LOCK(m.mutex); |
| VarEntry* entry = m.seek(_name); |
| if (entry == NULL) { |
| entry = &m[_name]; |
| entry->var = this; |
| entry->display_filter = display_filter; |
| return 0; |
| } |
| } |
| RELEASE_ASSERT_VERBOSE(!FLAGS_bvar_abort_on_same_name, |
| "Abort due to name conflict"); |
| if (!s_bvar_may_abort) { |
| // Mark name conflict occurs, If this conflict happens before |
| // initialization of bvar_abort_on_same_name, the validator will |
| // abort the program if needed. |
| s_bvar_may_abort = true; |
| } |
| |
| LOG(ERROR) << "Already exposed `" << _name << "' whose value is `" |
| << describe_exposed(_name) << '\''; |
| _name.clear(); |
| return -1; |
| } |
| |
| bool Variable::is_hidden() const { |
| return _name.empty(); |
| } |
| |
| bool Variable::hide() { |
| if (_name.empty()) { |
| return false; |
| } |
| VarMapWithLock& m = get_var_map(_name); |
| BAIDU_SCOPED_LOCK(m.mutex); |
| VarEntry* entry = m.seek(_name); |
| if (entry) { |
| CHECK_EQ(1UL, m.erase(_name)); |
| } else { |
| CHECK(false) << "`" << _name << "' must exist"; |
| } |
| _name.clear(); |
| return true; |
| } |
| |
| void Variable::list_exposed(std::vector<std::string>* names, |
| DisplayFilter display_filter) { |
| if (names == NULL) { |
| return; |
| } |
| names->clear(); |
| if (names->capacity() < 32) { |
| names->reserve(count_exposed()); |
| } |
| VarMapWithLock* var_maps = get_var_maps(); |
| for (size_t i = 0; i < SUB_MAP_COUNT; ++i) { |
| VarMapWithLock& m = var_maps[i]; |
| std::unique_lock<pthread_mutex_t> mu(m.mutex); |
| size_t n = 0; |
| for (VarMap::const_iterator it = m.begin(); it != m.end(); ++it) { |
| if (++n >= 256/*max iterated one pass*/) { |
| VarMap::PositionHint hint; |
| m.save_iterator(it, &hint); |
| n = 0; |
| mu.unlock(); // yield |
| mu.lock(); |
| it = m.restore_iterator(hint); |
| if (it == m.begin()) { // resized |
| names->clear(); |
| } |
| if (it == m.end()) { |
| break; |
| } |
| } |
| if (it->second.display_filter & display_filter) { |
| names->push_back(it->first); |
| } |
| } |
| } |
| } |
| |
| size_t Variable::count_exposed() { |
| size_t n = 0; |
| VarMapWithLock* var_maps = get_var_maps(); |
| for (size_t i = 0; i < SUB_MAP_COUNT; ++i) { |
| n += var_maps[i].size(); |
| } |
| return n; |
| } |
| |
| int Variable::describe_exposed(const std::string& name, std::ostream& os, |
| bool quote_string, |
| DisplayFilter display_filter) { |
| VarMapWithLock& m = get_var_map(name); |
| BAIDU_SCOPED_LOCK(m.mutex); |
| VarEntry* p = m.seek(name); |
| if (p == NULL) { |
| return -1; |
| } |
| if (!(display_filter & p->display_filter)) { |
| return -1; |
| } |
| p->var->describe(os, quote_string); |
| return 0; |
| } |
| |
| std::string Variable::describe_exposed(const std::string& name, |
| bool quote_string, |
| DisplayFilter display_filter) { |
| std::ostringstream oss; |
| if (describe_exposed(name, oss, quote_string, display_filter) == 0) { |
| return oss.str(); |
| } |
| return std::string(); |
| } |
| |
| std::string Variable::get_description() const { |
| std::ostringstream os; |
| describe(os, false); |
| return os.str(); |
| } |
| |
| #ifdef BAIDU_INTERNAL |
| void Variable::get_value(boost::any* value) const { |
| std::ostringstream os; |
| describe(os, false); |
| *value = os.str(); |
| } |
| #endif |
| |
| int Variable::describe_series_exposed(const std::string& name, |
| std::ostream& os, |
| const SeriesOptions& options) { |
| VarMapWithLock& m = get_var_map(name); |
| BAIDU_SCOPED_LOCK(m.mutex); |
| VarEntry* p = m.seek(name); |
| if (p == NULL) { |
| return -1; |
| } |
| return p->var->describe_series(os, options); |
| } |
| |
| #ifdef BAIDU_INTERNAL |
| int Variable::get_exposed(const std::string& name, boost::any* value) { |
| VarMapWithLock& m = get_var_map(name); |
| BAIDU_SCOPED_LOCK(m.mutex); |
| VarEntry* p = m.seek(name); |
| if (p == NULL) { |
| return -1; |
| } |
| p->var->get_value(value); |
| return 0; |
| } |
| #endif |
| |
| // TODO(gejun): This is copied from otherwhere, common it if possible. |
| |
| // Underlying buffer to store logs. Comparing to using std::ostringstream |
| // directly, this utility exposes more low-level methods so that we avoid |
| // creation of std::string which allocates memory internally. |
| class CharArrayStreamBuf : public std::streambuf { |
| public: |
| explicit CharArrayStreamBuf() : _data(NULL), _size(0) {} |
| ~CharArrayStreamBuf(); |
| |
| int overflow(int ch) override; |
| int sync() override; |
| void reset(); |
| butil::StringPiece data() { |
| return butil::StringPiece(pbase(), pptr() - pbase()); |
| } |
| |
| private: |
| char* _data; |
| size_t _size; |
| }; |
| |
| CharArrayStreamBuf::~CharArrayStreamBuf() { |
| free(_data); |
| } |
| |
| int CharArrayStreamBuf::overflow(int ch) { |
| if (ch == std::streambuf::traits_type::eof()) { |
| return ch; |
| } |
| size_t new_size = std::max(_size * 3 / 2, (size_t)64); |
| char* new_data = (char*)malloc(new_size); |
| if (BAIDU_UNLIKELY(new_data == NULL)) { |
| setp(NULL, NULL); |
| return std::streambuf::traits_type::eof(); |
| } |
| memcpy(new_data, _data, _size); |
| free(_data); |
| _data = new_data; |
| const size_t old_size = _size; |
| _size = new_size; |
| setp(_data, _data + new_size); |
| pbump(old_size); |
| // if size == 1, this function will call overflow again. |
| return sputc(ch); |
| } |
| |
| int CharArrayStreamBuf::sync() { |
| // data are already there. |
| return 0; |
| } |
| |
| void CharArrayStreamBuf::reset() { |
| setp(_data, _data + _size); |
| } |
| |
| |
| // Written by Jack Handy |
| // <A href="mailto:jakkhandy@hotmail.com">jakkhandy@hotmail.com</A> |
| inline bool wildcmp(const char* wild, const char* str, char question_mark) { |
| const char* cp = NULL; |
| const char* mp = NULL; |
| |
| while (*str && *wild != '*') { |
| if (*wild != *str && *wild != question_mark) { |
| return false; |
| } |
| ++wild; |
| ++str; |
| } |
| |
| while (*str) { |
| if (*wild == '*') { |
| if (!*++wild) { |
| return true; |
| } |
| mp = wild; |
| cp = str+1; |
| } else if (*wild == *str || *wild == question_mark) { |
| ++wild; |
| ++str; |
| } else { |
| wild = mp; |
| str = cp++; |
| } |
| } |
| |
| while (*wild == '*') { |
| ++wild; |
| } |
| return !*wild; |
| } |
| |
| class WildcardMatcher { |
| public: |
| WildcardMatcher(const std::string& wildcards, |
| char question_mark, |
| bool on_both_empty) |
| : _question_mark(question_mark) |
| , _on_both_empty(on_both_empty) { |
| if (wildcards.empty()) { |
| return; |
| } |
| std::string name; |
| const char wc_pattern[3] = { '*', question_mark, '\0' }; |
| for (butil::StringMultiSplitter sp(wildcards.c_str(), ",;"); |
| sp != NULL; ++sp) { |
| name.assign(sp.field(), sp.length()); |
| if (name.find_first_of(wc_pattern) != std::string::npos) { |
| if (_wcs.empty()) { |
| _wcs.reserve(8); |
| } |
| _wcs.push_back(name); |
| } else { |
| _exact.insert(name); |
| } |
| } |
| } |
| |
| bool match(const std::string& name) const { |
| if (!_exact.empty()) { |
| if (_exact.find(name) != _exact.end()) { |
| return true; |
| } |
| } else if (_wcs.empty()) { |
| return _on_both_empty; |
| } |
| for (size_t i = 0; i < _wcs.size(); ++i) { |
| if (wildcmp(_wcs[i].c_str(), name.c_str(), _question_mark)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| const std::vector<std::string>& wildcards() const { return _wcs; } |
| const std::set<std::string>& exact_names() const { return _exact; } |
| |
| private: |
| char _question_mark; |
| bool _on_both_empty; |
| std::vector<std::string> _wcs; |
| std::set<std::string> _exact; |
| }; |
| |
| DumpOptions::DumpOptions() |
| : quote_string(true) |
| , question_mark('?') |
| , display_filter(DISPLAY_ON_PLAIN_TEXT) |
| {} |
| |
| int Variable::dump_exposed(Dumper* dumper, const DumpOptions* poptions) { |
| if (NULL == dumper) { |
| LOG(ERROR) << "Parameter[dumper] is NULL"; |
| return -1; |
| } |
| DumpOptions opt; |
| if (poptions) { |
| opt = *poptions; |
| } |
| CharArrayStreamBuf streambuf; |
| std::ostream os(&streambuf); |
| int count = 0; |
| WildcardMatcher black_matcher(opt.black_wildcards, |
| opt.question_mark, |
| false); |
| WildcardMatcher white_matcher(opt.white_wildcards, |
| opt.question_mark, |
| true); |
| |
| std::ostringstream dumpped_info; |
| const bool log_dummped = FLAGS_bvar_log_dumpped; |
| |
| if (white_matcher.wildcards().empty() && |
| !white_matcher.exact_names().empty()) { |
| for (std::set<std::string>::const_iterator |
| it = white_matcher.exact_names().begin(); |
| it != white_matcher.exact_names().end(); ++it) { |
| const std::string& name = *it; |
| if (!black_matcher.match(name)) { |
| if (bvar::Variable::describe_exposed( |
| name, os, opt.quote_string, opt.display_filter) != 0) { |
| continue; |
| } |
| if (log_dummped) { |
| dumpped_info << '\n' << name << ": " << streambuf.data(); |
| } |
| if (!dumper->dump(name, streambuf.data())) { |
| return -1; |
| } |
| streambuf.reset(); |
| ++count; |
| } |
| } |
| } else { |
| // Have to iterate all variables. |
| std::vector<std::string> varnames; |
| bvar::Variable::list_exposed(&varnames, opt.display_filter); |
| // Sort the names to make them more readable. |
| std::sort(varnames.begin(), varnames.end()); |
| for (std::vector<std::string>::const_iterator |
| it = varnames.begin(); it != varnames.end(); ++it) { |
| const std::string& name = *it; |
| if (white_matcher.match(name) && !black_matcher.match(name)) { |
| if (bvar::Variable::describe_exposed( |
| name, os, opt.quote_string, opt.display_filter) != 0) { |
| continue; |
| } |
| if (log_dummped) { |
| dumpped_info << '\n' << name << ": " << streambuf.data(); |
| } |
| if (!dumper->dump(name, streambuf.data())) { |
| return -1; |
| } |
| streambuf.reset(); |
| ++count; |
| } |
| } |
| } |
| if (log_dummped) { |
| LOG(INFO) << "Dumpped variables:" << dumpped_info.str(); |
| } |
| return count; |
| } |
| |
| |
| // ============= export to files ============== |
| |
| std::string read_command_name() { |
| std::ifstream fin("/proc/self/stat"); |
| if (!fin.is_open()) { |
| return std::string(); |
| } |
| int pid = 0; |
| std::string command_name; |
| fin >> pid >> command_name; |
| if (!fin.good()) { |
| return std::string(); |
| } |
| // Although the man page says the command name is in parenthesis, for |
| // safety we normalize the name. |
| std::string s; |
| if (command_name.size() >= 2UL && command_name[0] == '(' && |
| butil::back_char(command_name) == ')') { |
| // remove parenthesis. |
| to_underscored_name(&s, |
| butil::StringPiece(command_name.data() + 1, |
| command_name.size() - 2UL)); |
| } else { |
| to_underscored_name(&s, command_name); |
| } |
| return s; |
| } |
| |
| class FileDumper : public Dumper { |
| public: |
| FileDumper(const std::string& filename, butil::StringPiece s/*prefix*/) |
| : _filename(filename), _fp(NULL) { |
| // setting prefix. |
| // remove trailing spaces. |
| const char* p = s.data() + s.size(); |
| for (; p != s.data() && isspace(p[-1]); --p) {} |
| s.remove_suffix(s.data() + s.size() - p); |
| // normalize it. |
| if (!s.empty()) { |
| to_underscored_name(&_prefix, s); |
| if (butil::back_char(_prefix) != '_') { |
| _prefix.push_back('_'); |
| } |
| } |
| } |
| |
| ~FileDumper() { |
| close(); |
| } |
| void close() { |
| if (_fp) { |
| fclose(_fp); |
| _fp = NULL; |
| } |
| } |
| |
| protected: |
| bool dump_impl(const std::string& name, const butil::StringPiece& desc, const std::string& separator) { |
| if (_fp == NULL) { |
| butil::File::Error error; |
| butil::FilePath dir = butil::FilePath(_filename).DirName(); |
| if (!butil::CreateDirectoryAndGetError(dir, &error)) { |
| LOG(ERROR) << "Fail to create directory=`" << dir.value() |
| << "', " << error; |
| return false; |
| } |
| _fp = fopen(_filename.c_str(), "w"); |
| if (NULL == _fp) { |
| LOG(ERROR) << "Fail to open " << _filename; |
| return false; |
| } |
| } |
| if (fprintf(_fp, "%.*s%.*s %.*s %.*s\r\n", |
| (int)_prefix.size(), _prefix.data(), |
| (int)name.size(), name.data(), |
| (int)separator.size(), separator.data(), |
| (int)desc.size(), desc.data()) < 0) { |
| PLOG(ERROR) << "Fail to write into " << _filename; |
| return false; |
| } |
| return true; |
| } |
| private: |
| std::string _filename; |
| FILE* _fp; |
| std::string _prefix; |
| }; |
| |
| class CommonFileDumper : public FileDumper { |
| public: |
| CommonFileDumper(const std::string& filename, butil::StringPiece prefix) |
| : FileDumper(filename, prefix) |
| , _separator(":") {} |
| bool dump(const std::string& name, const butil::StringPiece& desc) { |
| return dump_impl(name, desc, _separator); |
| } |
| private: |
| std::string _separator; |
| }; |
| |
| class PrometheusFileDumper : public FileDumper { |
| public: |
| PrometheusFileDumper(const std::string& filename, butil::StringPiece prefix) |
| : FileDumper(filename, prefix) |
| , _separator(" ") {} |
| bool dump(const std::string& name, const butil::StringPiece& desc) { |
| return dump_impl(name, desc, _separator); |
| } |
| private: |
| std::string _separator; |
| }; |
| |
| class FileDumperGroup : public Dumper { |
| public: |
| FileDumperGroup(std::string tabs, std::string filename, |
| butil::StringPiece s/*prefix*/) { |
| butil::FilePath path(filename); |
| if (path.FinalExtension() == ".data") { |
| // .data will be appended later |
| path = path.RemoveFinalExtension(); |
| } |
| |
| for (butil::KeyValuePairsSplitter sp(tabs, ';', '='); sp; ++sp) { |
| std::string key = sp.key().as_string(); |
| std::string value = sp.value().as_string(); |
| FileDumper *f = new CommonFileDumper( |
| path.AddExtension(key).AddExtension("data").value(), s); |
| WildcardMatcher *m = new WildcardMatcher(value, '?', true); |
| dumpers.emplace_back(f, m); |
| } |
| dumpers.emplace_back( |
| new CommonFileDumper(path.AddExtension("data").value(), s), |
| (WildcardMatcher *)NULL); |
| } |
| ~FileDumperGroup() { |
| for (size_t i = 0; i < dumpers.size(); ++i) { |
| delete dumpers[i].first; |
| delete dumpers[i].second; |
| } |
| dumpers.clear(); |
| } |
| |
| bool dump(const std::string& name, const butil::StringPiece& desc) override { |
| for (size_t i = 0; i < dumpers.size() - 1; ++i) { |
| if (dumpers[i].second->match(name)) { |
| return dumpers[i].first->dump(name, desc); |
| } |
| } |
| // dump to default file |
| return dumpers.back().first->dump(name, desc); |
| } |
| private: |
| std::vector<std::pair<FileDumper *, WildcardMatcher*> > dumpers; |
| }; |
| |
| static pthread_once_t dumping_thread_once = PTHREAD_ONCE_INIT; |
| static bool created_dumping_thread = false; |
| static pthread_mutex_t dump_mutex = PTHREAD_MUTEX_INITIALIZER; |
| static pthread_cond_t dump_cond = PTHREAD_COND_INITIALIZER; |
| |
| DEFINE_bool(bvar_dump, false, |
| "Create a background thread dumping all bvar periodically, " |
| "all bvar_dump_* flags are not effective when this flag is off"); |
| DEFINE_int32(bvar_dump_interval, 10, "Seconds between consecutive dump"); |
| DEFINE_string(bvar_dump_file, "monitor/bvar.<app>.data", "Dump bvar into this file"); |
| DEFINE_string(bvar_dump_include, "", "Dump bvar matching these wildcards, " |
| "separated by semicolon(;), empty means including all"); |
| DEFINE_string(bvar_dump_exclude, "", "Dump bvar excluded from these wildcards, " |
| "separated by semicolon(;), empty means no exclusion"); |
| DEFINE_string(bvar_dump_prefix, "<app>", "Every dumped name starts with this prefix"); |
| DEFINE_string(bvar_dump_tabs, "latency=*_latency*" |
| ";qps=*_qps*" |
| ";error=*_error*" |
| ";system=*process_*,*malloc_*,*kernel_*", |
| "Dump bvar into different tabs according to the filters (separated by semicolon), " |
| "format: *(tab_name=wildcards;)"); |
| |
| DEFINE_bool(mbvar_dump, false, |
| "Create a background thread dumping(shares the same thread as bvar_dump) all mbvar periodically, " |
| "all mbvar_dump_* flags are not effective when this flag is off"); |
| DEFINE_string(mbvar_dump_file, "monitor/mbvar.<app>.data", "Dump mbvar into this file"); |
| DEFINE_string(mbvar_dump_prefix, "<app>", "Every dumped name starts with this prefix"); |
| DEFINE_string(mbvar_dump_format, "common", "Dump mbvar write format"); |
| |
| #if !defined(BVAR_NOT_LINK_DEFAULT_VARIABLES) |
| // Expose bvar-releated gflags so that they're collected by noah. |
| // Maybe useful when debugging process of monitoring. |
| static GFlag s_gflag_bvar_dump_interval("bvar_dump_interval"); |
| #endif |
| |
| // The background thread to export all bvar periodically. |
| static void* dumping_thread(void*) { |
| // NOTE: this variable was declared as static <= r34381, which was |
| // destructed when program exits and caused coredumps. |
| butil::PlatformThread::SetNameSimple("bvar_dumper"); |
| const std::string command_name = read_command_name(); |
| std::string last_filename; |
| std::string mbvar_last_filename; |
| while (1) { |
| // We can't access string flags directly because it's thread-unsafe. |
| std::string filename; |
| DumpOptions options; |
| std::string prefix; |
| std::string tabs; |
| std::string mbvar_filename; |
| std::string mbvar_prefix; |
| std::string mbvar_format; |
| if (!GFLAGS_NAMESPACE::GetCommandLineOption("bvar_dump_file", &filename)) { |
| LOG(ERROR) << "Fail to get gflag bvar_dump_file"; |
| return NULL; |
| } |
| if (!GFLAGS_NAMESPACE::GetCommandLineOption("bvar_dump_include", |
| &options.white_wildcards)) { |
| LOG(ERROR) << "Fail to get gflag bvar_dump_include"; |
| return NULL; |
| } |
| if (!GFLAGS_NAMESPACE::GetCommandLineOption("bvar_dump_exclude", |
| &options.black_wildcards)) { |
| LOG(ERROR) << "Fail to get gflag bvar_dump_exclude"; |
| return NULL; |
| } |
| if (!GFLAGS_NAMESPACE::GetCommandLineOption("bvar_dump_prefix", &prefix)) { |
| LOG(ERROR) << "Fail to get gflag bvar_dump_prefix"; |
| return NULL; |
| } |
| if (!GFLAGS_NAMESPACE::GetCommandLineOption("bvar_dump_tabs", &tabs)) { |
| LOG(ERROR) << "Fail to get gflags bvar_dump_tabs"; |
| return NULL; |
| } |
| |
| // We can't access string flags directly because it's thread-unsafe. |
| if (!GFLAGS_NAMESPACE::GetCommandLineOption("mbvar_dump_file", &mbvar_filename)) { |
| LOG(ERROR) << "Fail to get gflag mbvar_dump_file"; |
| return NULL; |
| } |
| if (!GFLAGS_NAMESPACE::GetCommandLineOption("mbvar_dump_prefix", &mbvar_prefix)) { |
| LOG(ERROR) << "Fail to get gflag mbvar_dump_prefix"; |
| return NULL; |
| } |
| if (!GFLAGS_NAMESPACE::GetCommandLineOption("mbvar_dump_format", &mbvar_format)) { |
| LOG(ERROR) << "Fail to get gflag mbvar_dump_format"; |
| return NULL; |
| } |
| |
| if (FLAGS_bvar_dump && !filename.empty()) { |
| // Replace first <app> in filename with program name. We can't use |
| // pid because a same binary should write the data to the same |
| // place, otherwise restarting of app may confuse noah with a lot |
| // of *.data. noah takes 1.5 days to figure out that some data is |
| // outdated and to be removed. |
| const size_t pos = filename.find("<app>"); |
| if (pos != std::string::npos) { |
| filename.replace(pos, 5/*<app>*/, command_name); |
| } |
| if (last_filename != filename) { |
| last_filename = filename; |
| LOG(INFO) << "Write all bvar to " << filename << " every " |
| << FLAGS_bvar_dump_interval << " seconds."; |
| } |
| const size_t pos2 = prefix.find("<app>"); |
| if (pos2 != std::string::npos) { |
| prefix.replace(pos2, 5/*<app>*/, command_name); |
| } |
| FileDumperGroup dumper(tabs, filename, prefix); |
| int nline = Variable::dump_exposed(&dumper, &options); |
| if (nline < 0) { |
| LOG(ERROR) << "Fail to dump vars into " << filename; |
| } |
| } |
| |
| // Dump multi dimension bvar |
| if (FLAGS_mbvar_dump && !mbvar_filename.empty()) { |
| // Replace first <app> in filename with program name. We can't use |
| // pid because a same binary should write the data to the same |
| // place, otherwise restarting of app may confuse noah with a lot |
| // of *.data. noah takes 1.5 days to figure out that some data is |
| // outdated and to be removed. |
| const size_t pos = mbvar_filename.find("<app>"); |
| if (pos != std::string::npos) { |
| mbvar_filename.replace(pos, 5/*<app>*/, command_name); |
| } |
| if (mbvar_last_filename != mbvar_filename) { |
| mbvar_last_filename = mbvar_filename; |
| LOG(INFO) << "Write all mbvar to " << mbvar_filename << " every " |
| << FLAGS_bvar_dump_interval << " seconds."; |
| } |
| const size_t pos2 = mbvar_prefix.find("<app>"); |
| if (pos2 != std::string::npos) { |
| mbvar_prefix.replace(pos2, 5/*<app>*/, command_name); |
| } |
| |
| Dumper* dumper = NULL; |
| if ("common" == mbvar_format) { |
| dumper = new CommonFileDumper(mbvar_filename, mbvar_prefix); |
| } else if ("prometheus" == mbvar_format) { |
| dumper = new PrometheusFileDumper(mbvar_filename, mbvar_prefix); |
| } |
| int nline = MVariableBase::dump_exposed(dumper, &options); |
| if (nline < 0) { |
| LOG(ERROR) << "Fail to dump mvars into " << filename; |
| } |
| delete dumper; |
| dumper = NULL; |
| } |
| |
| // We need to separate the sleeping into a long interruptible sleep |
| // and a short uninterruptible sleep. Doing this because we wake up |
| // this thread in gflag validators. If this thread dumps just after |
| // waking up from the condition, the gflags may not even be updated. |
| const int post_sleep_ms = 50; |
| int cond_sleep_ms = FLAGS_bvar_dump_interval * 1000 - post_sleep_ms; |
| if (cond_sleep_ms < 0) { |
| LOG(ERROR) << "Bad cond_sleep_ms=" << cond_sleep_ms; |
| cond_sleep_ms = 10000; |
| } |
| timespec deadline = butil::milliseconds_from_now(cond_sleep_ms); |
| pthread_mutex_lock(&dump_mutex); |
| pthread_cond_timedwait(&dump_cond, &dump_mutex, &deadline); |
| pthread_mutex_unlock(&dump_mutex); |
| usleep(post_sleep_ms * 1000); |
| } |
| } |
| |
| static void launch_dumping_thread() { |
| pthread_t thread_id; |
| int rc = pthread_create(&thread_id, NULL, dumping_thread, NULL); |
| if (rc != 0) { |
| LOG(FATAL) << "Fail to launch dumping thread: " << berror(rc); |
| return; |
| } |
| // Detach the thread because no one would join it. |
| CHECK_EQ(0, pthread_detach(thread_id)); |
| created_dumping_thread = true; |
| } |
| |
| // Start dumping_thread for only once. |
| static bool enable_dumping_thread() { |
| pthread_once(&dumping_thread_once, launch_dumping_thread); |
| return created_dumping_thread; |
| } |
| |
| static bool validate_bvar_dump(const char*, bool enabled) { |
| if (enabled) { |
| return enable_dumping_thread(); |
| } |
| return true; |
| } |
| BUTIL_VALIDATE_GFLAG(bvar_dump, validate_bvar_dump); |
| |
| // validators (to make these gflags reloadable in brpc) |
| static bool validate_bvar_dump_interval(const char*, int32_t v) { |
| // FIXME: -bvar_dump_interval is actually unreloadable but we need to |
| // check validity of it, so we still add this validator. In practice |
| // this is just fine since people rarely have the intention of modifying |
| // this flag at runtime. |
| if (v < 1) { |
| LOG(ERROR) << "Invalid bvar_dump_interval=" << v; |
| return false; |
| } |
| return true; |
| } |
| BUTIL_VALIDATE_GFLAG(bvar_dump_interval, validate_bvar_dump_interval); |
| |
| static bool wakeup_dumping_thread(const char*, const std::string&) { |
| // We're modifying a flag, wake up dumping_thread to generate |
| // a new file soon. |
| pthread_cond_signal(&dump_cond); |
| return true; |
| } |
| |
| const bool ALLOW_UNUSED dummy_bvar_dump_file = GFLAGS_NAMESPACE::RegisterFlagValidator( |
| &FLAGS_bvar_dump_file, wakeup_dumping_thread); |
| const bool ALLOW_UNUSED dummy_bvar_dump_filter = GFLAGS_NAMESPACE::RegisterFlagValidator( |
| &FLAGS_bvar_dump_include, wakeup_dumping_thread); |
| const bool ALLOW_UNUSED dummy_bvar_dump_exclude = GFLAGS_NAMESPACE::RegisterFlagValidator( |
| &FLAGS_bvar_dump_exclude, wakeup_dumping_thread); |
| const bool ALLOW_UNUSED dummy_bvar_dump_prefix = GFLAGS_NAMESPACE::RegisterFlagValidator( |
| &FLAGS_bvar_dump_prefix, wakeup_dumping_thread); |
| const bool ALLOW_UNUSED dummy_bvar_dump_tabs = GFLAGS_NAMESPACE::RegisterFlagValidator( |
| &FLAGS_bvar_dump_tabs, wakeup_dumping_thread); |
| |
| BUTIL_VALIDATE_GFLAG(mbvar_dump, validate_bvar_dump); |
| const bool ALLOW_UNUSED dummy_mbvar_dump_prefix = GFLAGS_NAMESPACE::RegisterFlagValidator( |
| &FLAGS_mbvar_dump_prefix, wakeup_dumping_thread); |
| const bool ALLOW_UNUSED dump_mbvar_dump_file = GFLAGS_NAMESPACE::RegisterFlagValidator( |
| &FLAGS_mbvar_dump_file, wakeup_dumping_thread); |
| |
| static bool validate_mbvar_dump_format(const char*, const std::string& format) { |
| if (format != "common" |
| && format != "prometheus") { |
| LOG(ERROR) << "Invalid mbvar_dump_format=" << format; |
| return false; |
| } |
| |
| // We're modifying a flag, wake up dumping_thread to generate |
| // a new file soon. |
| pthread_cond_signal(&dump_cond); |
| return true; |
| } |
| |
| const bool ALLOW_UNUSED dummy_mbvar_dump_format = GFLAGS_NAMESPACE::RegisterFlagValidator( |
| &FLAGS_mbvar_dump_format, validate_mbvar_dump_format); |
| |
| void to_underscored_name(std::string* name, const butil::StringPiece& src) { |
| name->reserve(name->size() + src.size() + 8/*just guess*/); |
| for (const char* p = src.data(); p != src.data() + src.size(); ++p) { |
| if (isalpha(*p)) { |
| if (*p < 'a') { // upper cases |
| if (p != src.data() && !isupper(p[-1]) && |
| butil::back_char(*name) != '_') { |
| name->push_back('_'); |
| } |
| name->push_back(*p - 'A' + 'a'); |
| } else { |
| name->push_back(*p); |
| } |
| } else if (isdigit(*p)) { |
| name->push_back(*p); |
| } else if (name->empty() || butil::back_char(*name) != '_') { |
| name->push_back('_'); |
| } |
| } |
| } |
| |
| // UT don't need default variables. |
| #if !defined(BVAR_NOT_LINK_DEFAULT_VARIABLES) |
| // Without these, default_variables.o are stripped. |
| // At least working in gcc 4.8 |
| extern int do_link_default_variables; |
| int dummy = do_link_default_variables; |
| #endif |
| |
| } // namespace bvar |