Crow  0.0.4
crow.cpp
1 /*
2  _____ _____ _____ _ _ _
3 | | __ | | | | | Crow - a Sentry client for C++
4 | --| -| | | | | | version 0.0.4
5 |_____|__|__|_____|_____| https://github.com/nlohmann/crow
6 
7 Licensed under the MIT License <http://opensource.org/licenses/MIT>.
8 SPDX-License-Identifier: MIT
9 Copyright (c) 2018 Niels Lohmann <http://nlohmann.me>.
10 
11 Permission is hereby granted, free of charge, to any person obtaining a copy
12 of this software and associated documentation files (the "Software"), to deal
13 in the Software without restriction, including without limitation the rights
14 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 copies of the Software, and to permit persons to whom the Software is
16 furnished to do so, subject to the following conditions:
17 
18 The above copyright notice and this permission notice shall be included in all
19 copies or substantial portions of the Software.
20 
21 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 SOFTWARE.
28 */
29 
30 #include <cstdlib>
31 #include <cstddef>
32 #include <cstring>
33 #include <ctime>
34 #include <exception>
35 #include <future>
36 #include <mutex>
37 #include <regex>
38 #include <stdexcept>
39 #include <crow/crow.hpp>
40 #include <src/crow_config.hpp>
41 #include <src/crow_utilities.hpp>
42 #include <thirdparty/curl_wrapper/curl_wrapper.hpp>
43 #include <thirdparty/json/json.hpp>
44 
45 using json = nlohmann::json;
46 
47 namespace nlohmann
48 {
49 crow* crow::m_client_that_installed_termination_handler = nullptr;
50 
51 crow::crow(const std::string& dsn,
52  const json& context,
53  const bool install_handlers)
54  : m_enabled(not dsn.empty())
55 {
56  // process DSN
57  if (not dsn.empty())
58  {
59  const std::regex dsn_regex("(http[s]?)://([^:]+):([^@]+)@([^/]+)/([0-9]+)");
60  std::smatch pieces_match;
61 
62  if (std::regex_match(dsn, pieces_match, dsn_regex) and pieces_match.size() == 6)
63  {
64  const auto scheme = pieces_match.str(1);
65  m_public_key = pieces_match.str(2);
66  m_secret_key = pieces_match.str(3);
67  const auto host = pieces_match.str(4);
68  const auto project_id = pieces_match.str(5);
69  m_store_url = scheme + "://" + host + "/api/" + project_id + "/store/";
70  }
71  else
72  {
73  throw std::invalid_argument("DNS " + dsn + " is invalid");
74  }
75  }
76 
77  // manage context
78  clear_context();
79  merge_context(context);
80 
81  // install termination handler
82  if (install_handlers)
83  {
85  }
86 }
87 
89 {
90  if (existing_termination_handler == nullptr)
91  {
92  existing_termination_handler = std::set_terminate([]() {});
93  std::set_terminate(&new_termination_handler);
94 
95  // we remember this client, because we will use it to report
96  // uncaught exceptions with it
97  m_client_that_installed_termination_handler = this;
98  }
99 }
100 
101 crow::crow(const crow& other)
102  : m_enabled(other.m_enabled)
103  , m_public_key(other.m_public_key)
104  , m_secret_key(other.m_secret_key)
105  , m_store_url(other.m_store_url)
106  , m_payload(other.m_payload)
107 {}
108 
110 {
111  // wait fur running request to finish
112  if (m_pending_future.valid())
113  {
114  m_pending_future.wait();
115  }
116 }
117 
118 void crow::capture_message(const std::string& message,
119  const json& attributes,
120  const bool asynchronous)
121 {
122  std::lock_guard<std::mutex> lock(m_payload_mutex);
123  m_payload["message"] = message;
124  m_payload["event_id"] = nlohmann::crow_utilities::generate_uuid();
125  m_payload["timestamp"] = nlohmann::crow_utilities::get_iso8601();
126 
127  if (attributes.is_object())
128  {
129  // logger
130  auto logger = attributes.find("logger");
131  if (logger != attributes.end())
132  {
133  m_payload["logger"] = *logger;
134  }
135 
136  // level
137  m_payload["level"] = attributes.value("level", "error");
138 
139  // context
140  auto context = attributes.find("context");
141  if (context != attributes.end())
142  {
143  merge_context(*context);
144  }
145 
146  // extra
147  auto extra = attributes.find("extra");
148  if (extra != attributes.end())
149  {
150  m_payload["extra"] = *extra;
151  }
152  }
153 
154  enqueue_post(asynchronous);
155 }
156 
157 
158 void crow::capture_exception(const std::exception& exception,
159  const json& context,
160  const bool asynchronous,
161  const bool handled)
162 {
163  std::stringstream thread_id;
164  thread_id << std::this_thread::get_id();
165  std::lock_guard<std::mutex> lock(m_payload_mutex);
166  m_payload["exception"].push_back({{"type", crow_utilities::pretty_name(typeid(exception).name())},
167  {"value", exception.what()},
168  {"module", crow_utilities::pretty_name(typeid(exception).name(), true)},
169  {"mechanism", {{"handled", handled}, {"description", handled ? "handled exception" : "unhandled exception"}}},
170  {"stacktrace", {{"frames", crow_utilities::get_backtrace()}}},
171  {"thread_id", thread_id.str()}});
172  m_payload["event_id"] = crow_utilities::generate_uuid();
173  m_payload["timestamp"] = nlohmann::crow_utilities::get_iso8601();
174 
175  // add given context
176  merge_context(context);
177 
178  enqueue_post(asynchronous);
179 }
180 
181 void crow::add_breadcrumb(const std::string& message,
182  const json& attributes)
183 {
184  json breadcrumb =
185  {
186  {"event_id", crow_utilities::generate_uuid()},
187  {"message", message},
188  {"level", "info"},
189  {"type", "default"},
190  {"category", "log"},
191  {"timestamp", crow_utilities::get_timestamp()}
192  };
193 
194  if (attributes.is_object())
195  {
196  // type
197  auto type = attributes.find("type");
198  if (type != attributes.end())
199  {
200  breadcrumb["type"] = *type;
201  }
202 
203  // level
204  auto level = attributes.find("level");
205  if (level != attributes.end())
206  {
207  breadcrumb["level"] = *level;
208  }
209 
210  // category
211  auto category = attributes.find("category");
212  if (category != attributes.end())
213  {
214  breadcrumb["category"] = *category;
215  }
216 
217  // data
218  auto data = attributes.find("data");
219  if (data != attributes.end())
220  {
221  breadcrumb["data"] = *data;
222  }
223  }
224 
225  std::lock_guard<std::mutex> lock(m_payload_mutex);
226  m_payload["breadcrumbs"]["values"].push_back(std::move(breadcrumb));
227 }
228 
229 std::string crow::get_last_event_id() const
230 {
231  std::lock_guard<std::mutex> lock(m_pending_future_mutex);
232  if (m_pending_future.valid())
233  {
234  try
235  {
236  return json::parse(m_pending_future.get()).at("id");
237  }
238  catch (const json::exception&)
239  {
240  return "";
241  }
242  }
243  else
244  {
245  return "";
246  }
247 }
248 
249 const json& crow::get_context() const
250 {
251  return m_payload;
252 }
253 
254 void crow::add_user_context(const json& data)
255 {
256  std::lock_guard<std::mutex> lock(m_payload_mutex);
257  m_payload["user"].update(data);
258 }
259 
260 void crow::add_tags_context(const json& data)
261 {
262  std::lock_guard<std::mutex> lock(m_payload_mutex);
263  m_payload["tags"].update(data);
264 }
265 
266 void crow::add_request_context(const json& data)
267 {
268  std::lock_guard<std::mutex> lock(m_payload_mutex);
269  m_payload["request"].update(data);
270 }
271 
272 void crow::add_extra_context(const json& data)
273 {
274  std::lock_guard<std::mutex> lock(m_payload_mutex);
275  m_payload["extra"].update(data);
276 }
277 
278 void crow::merge_context(const json& context)
279 {
280  if (context.is_object())
281  {
282  std::lock_guard<std::mutex> lock(m_payload_mutex);
283  for (const auto& el : context.items())
284  {
285  if (el.key() == "user" or el.key() == "request" or el.key() == "extra" or el.key() == "tags")
286  {
287  m_payload[el.key()].update(el.value());
288  }
289  else
290  {
291  throw std::runtime_error("invalid context");
292  }
293  }
294  }
295 }
296 
298 {
299  std::lock_guard<std::mutex> lock(m_payload_mutex);
300  m_payload.clear();
301  m_payload["platform"] = "c";
302  m_payload["sdk"]["name"] = "crow";
303  m_payload["sdk"]["version"] = NLOHMANN_CROW_VERSION;
304 
305  // add context: app
306  m_payload["contexts"]["app"]["build_type"] = NLOHMANN_CROW_CMAKE_BUILD_TYPE;
307  m_payload["contexts"]["app"]["pointer_size"] = NLOHMANN_CROW_BITS;
308 
309  // add context: device
310  m_payload["contexts"]["device"]["arch"] = NLOHMANN_CROW_CMAKE_SYSTEM_PROCESSOR;
311  m_payload["contexts"]["device"]["name"] = NLOHMANN_CROW_HOSTNAME;
312  m_payload["contexts"]["device"]["model"] = NLOHMANN_CROW_SYSCTL_HW_MODEL;
313  m_payload["contexts"]["device"]["memory_size"] = NLOHMANN_CROW_TOTAL_PHYSICAL_MEMORY;
314 
315  // add context: os
316  m_payload["contexts"]["os"]["name"] = NLOHMANN_CROW_CMAKE_SYSTEM_NAME;
317  m_payload["contexts"]["os"]["version"] = NLOHMANN_CROW_OS_RELEASE;
318  if (not std::string(NLOHMANN_CROW_OS_VERSION).empty())
319  {
320  m_payload["contexts"]["os"]["build"] = NLOHMANN_CROW_OS_VERSION;
321  }
322  else
323  {
324  m_payload["contexts"]["os"]["build"] = NLOHMANN_CROW_CMAKE_SYSTEM_VERSION;
325  }
326  if (not std::string(NLOHMANN_CROW_UNAME).empty())
327  {
328  m_payload["contexts"]["os"]["kernel_version"] = NLOHMANN_CROW_UNAME;
329  }
330  else if (not std::string(NLOHMANN_CROW_SYSTEMINFO).empty())
331  {
332  m_payload["contexts"]["os"]["kernel_version"] = NLOHMANN_CROW_SYSTEMINFO;
333  }
334 
335  // add context: runtime
336  m_payload["contexts"]["runtime"]["name"] = NLOHMANN_CROW_CMAKE_CXX_COMPILER_ID;
337  m_payload["contexts"]["runtime"]["version"] = NLOHMANN_CROW_CMAKE_CXX_COMPILER_VERSION;
338  m_payload["contexts"]["runtime"]["detail"] = NLOHMANN_CROW_CXX;
339 
340  // add context: user
341  const char* user = getenv("USER");
342  if (user == nullptr)
343  {
344  user = getenv("USERNAME");
345  }
346  if (user)
347  {
348  m_payload["user"]["id"] = std::string(user) + "@" + NLOHMANN_CROW_HOSTNAME;
349  m_payload["user"]["username"] = user;
350  }
351 }
352 
353 std::string crow::post(json payload) const
354 {
355  if (m_enabled)
356  {
357  curl_wrapper curl;
358 
359  // add security header
360  std::string security_header = "X-Sentry-Auth: Sentry sentry_version=5,sentry_client=crow/";
361  security_header += std::string(NLOHMANN_CROW_VERSION) + ",sentry_timestamp=";
362  security_header += std::to_string(crow_utilities::get_timestamp());
363  security_header += ",sentry_key=" + m_public_key;
364  security_header += ",sentry_secret=" + m_secret_key;
365  curl.set_header(security_header.c_str());
366 
367  return curl.post(m_store_url, payload);
368  }
369 
370  return "";
371 }
372 
373 void crow::new_termination_handler()
374 {
375  assert(m_client_that_installed_termination_handler != nullptr);
376 
377  auto current_ex = std::current_exception();
378  if (current_ex)
379  {
380  m_client_that_installed_termination_handler->add_breadcrumb("uncaught exception", {{"type", "exceptiomn"}, {"level", "critical"}});
381  try
382  {
383  std::rethrow_exception(current_ex);
384  }
385  catch (const std::exception& e)
386  {
387  m_client_that_installed_termination_handler->capture_exception(e, nullptr, false, false);
388  }
389  }
390 
391  m_client_that_installed_termination_handler->existing_termination_handler();
392 }
393 
394 void crow::enqueue_post(const bool asynchronous)
395 {
396  std::lock_guard<std::mutex> lock(m_pending_future_mutex);
397 
398  // wait for running post requests
399  if (m_pending_future.valid())
400  {
401  m_pending_future.wait();
402  }
403 
404  // start a new post request
405  m_pending_future = std::async(std::launch::async, [this] { return post(m_payload); });
406  if (not asynchronous)
407  {
408  m_pending_future.wait();
409  }
410 }
411 
412 }
std::string get_iso8601()
get the current date and time as ISO 8601 string
json get_backtrace(int skip)
void add_user_context(const json &data)
add elements to the "user" context for future events
Definition: crow.cpp:254
void add_request_context(const json &data)
add elements to the "request" context for future events
Definition: crow.cpp:266
const json & get_context() const
return current context
Definition: crow.cpp:249
void merge_context(const json &context)
add context information to payload for future events
Definition: crow.cpp:278
void install_handler()
install termination handler to handle uncaught exceptions
Definition: crow.cpp:88
crow(const std::string &dsn, const json &context=nullptr, bool install_handlers=true)
create a client
Definition: crow.cpp:51
void add_extra_context(const json &data)
add elements to the "extra" context for future events
Definition: crow.cpp:272
void capture_exception(const std::exception &exception, const json &context=nullptr, bool asynchronous=true, bool handled=true)
capture an exception
Definition: crow.cpp:158
std::string get_last_event_id() const
return the id of the last reported event
Definition: crow.cpp:229
namespace for Niels Lohmann
Definition: crow.hpp:40
void clear_context()
reset context for future events
Definition: crow.cpp:297
~crow()
destructor
Definition: crow.cpp:109
std::int64_t get_timestamp()
get the seconds since epoch
void add_breadcrumb(const std::string &message, const json &attributes=nullptr)
add a breadcrumb to the current context
Definition: crow.cpp:181
a C++ client for Sentry
Definition: crow.hpp:47
std::string pretty_name(const char *type_id_name, const bool only_module)
return pretty type name
void capture_message(const std::string &message, const json &attributes=nullptr, bool asynchronous=true)
capture a message
Definition: crow.cpp:118
void add_tags_context(const json &data)
add elements to the "tags" context for future events
Definition: crow.cpp:260
std::string generate_uuid()
generate a UUID4 without dashes