sentry.cc
1 // Copyright 2018, Beeri 15. All rights reserved.
2 // Author: Roman Gershman (romange@gmail.com)
3 //
4 #include "util/sentry/sentry.h"
5 
6 #include <boost/beast/http/write.hpp> // For serializing req/resp to ostream
7 #include <cstring>
8 
9 #include "base/logging.h"
10 #include <glog/raw_logging.h>
11 
12 #include "strings/strcat.h"
13 #include "util/asio/glog_asio_sink.h"
14 #include "util/http/http_client.h"
15 
16 DEFINE_string(sentry_dsn, "", "Sentry DSN in the format <pivatekey>@hostname/<project_id>");
17 
18 namespace util {
19 using namespace ::boost;
20 using namespace ::std;
21 
22 namespace {
23 
24 struct Dsn {
25  string public_key;
26  string secret_key;
27  string hostname;
28  string url;
29 };
30 
31 class SentrySink : public GlogAsioSink {
32  public:
33  explicit SentrySink(Dsn dsn, IoContext* io_context);
34 
35  protected:
36  void HandleItem(const Item& item) final;
37 
38  bool ShouldIgnore(google::LogSeverity severity, const char* full_filename, int line) final {
39  return severity < google::GLOG_ERROR;
40  }
41 
42  void Cancel() final {
43  GlogAsioSink::Cancel();
44  client_.Shutdown();
45  VLOG(1) << "Sentry::Cancel End";
46  }
47 
48  private:
49  string GenSentryBody(const Item& item);
50 
51  http::Client client_;
52  Dsn dsn_;
53  string port_;
54 };
55 
56 /* The structure is as follows:
57 
58 curl -H 'X-Sentry-Auth: Sentry sentry_version=6, sentry_key=<private-key>' -i
59 -d '{"event_id": "<event_id>","culprit": "myfile.cc:12", "server_name": "someserver",
60 "message": "Error message", "level": "error", "platform": "c",
61 "timestamp": "2018-12-01T21:00:36"}' http://sentry.url/api/project_id/store/
62 
63 */
64 
65 SentrySink::SentrySink(Dsn dsn, IoContext* io_context) : client_(io_context), dsn_(std::move(dsn)) {
66  size_t pos = dsn_.hostname.find(':');
67  if (pos != string::npos) {
68  port_ = dsn_.hostname.substr(pos + 1);
69  dsn_.hostname.resize(pos);
70  } else {
71  port_ = "80";
72  }
73  std::string optional_secret_field = dsn_.secret_key.empty() ? "" :
74  absl::StrCat(", sentry_secret=", dsn_.secret_key);
75  client_.AddHeader("X-Sentry-Auth",
76  absl::StrCat("Sentry sentry_version=7",
77  ", sentry_key=", dsn_.public_key,
78  optional_secret_field));
79  client_.AddHeader("Content-Type", "application/json");
80  client_.AddHeader("Host", dsn_.hostname);
81  client_.AddHeader("User-Agent", "gaia-cpp/0.1");
82  dsn_.url = absl::StrCat("/api", dsn_.url, "/store/");
83 }
84 
85 void SentrySink::HandleItem(const Item& item) {
86  RAW_VLOG(2, "SentrySink::HandleItem");
87 
88  auto ec = client_.Connect(dsn_.hostname, port_);
89  if (ec) {
90  auto msg = ec.message();
91  RAW_VLOG(1, "Could not connect %s", msg.c_str());
92  ++lost_messages_;
93  return;
94  }
95 
96  http::Client::Response resp;
97  string body = GenSentryBody(item);
98  ec = client_.Send(http::Client::Verb::post, dsn_.url, body, &resp);
99 
100  if (ec) {
101  RAW_VLOG(1, "Could not send ");
102  ++lost_messages_;
103  }
104 }
105 
106 string SentrySink::GenSentryBody(const Item& item) {
107  string res = absl::StrCat(R"({"culprit":")", item.base_filename, ":", item.line,
108  R"(", "server_name":"TBD")");
109 
110  absl::StrAppend(&res,
111  ",\n"
112  R"( "message":")",
113  item.message,
114  R"(", "level":"error", "platform": "c++", "sdk": {"name": "sentry-cpp",
115  "version": "1.0.0"}, "timestamp":")");
116  absl::StrAppend(&res, 1900 + item.tm_time.tm_year, "-", item.tm_time.tm_mon + 1, "-",
117  item.tm_time.tm_mday, "T", item.tm_time.tm_hour, ":", item.tm_time.tm_min, ":",
118  item.tm_time.tm_sec);
119  absl::StrAppend(&res, R"("})");
120 
121  return res;
122 }
123 
124 bool ParseDsn(const string& dsn, Dsn* res) {
125  CHECK(!dsn.empty());
126  size_t kpos = dsn.find('@');
127  if (kpos == string::npos)
128  return false;
129  size_t protocol_sep_pos = dsn.find("://");
130  if (protocol_sep_pos == string::npos)
131  protocol_sep_pos = 0; // Nobody wrote http://, just assume it is http anyway.
132  else
133  protocol_sep_pos += 3;
134  std::string key = dsn.substr(protocol_sep_pos, kpos - protocol_sep_pos);
135  size_t colon_pos = key.find(':');
136  if (colon_pos == string::npos) {
137  res->public_key = key;
138  res->secret_key = "";
139  } else {
140  res->public_key = key.substr(0, colon_pos);
141  res->secret_key = key.substr(colon_pos + 1);
142  }
143  ++kpos;
144  size_t pos = dsn.find('/', kpos);
145  if (pos == string::npos)
146  return false;
147  res->hostname = dsn.substr(kpos, pos - kpos);
148  res->url = dsn.substr(pos);
149 
150  VLOG(1) << "Dsn is: " << res->public_key
151  << "|" << res->secret_key
152  << "|" << res->hostname
153  << "|" << res->url;
154  return true;
155 }
156 
157 } // namespace
158 
159 void EnableSentry(IoContext* context) {
160  static std::atomic<bool> ran_once(false);
161  CHECK(!ran_once.exchange(true)) << "EnableSentry called twice";
162 
163  std::string sentry_dsn = FLAGS_sentry_dsn;
164  if (sentry_dsn.empty()) {
165  if (const char *env = getenv("SENTRY_LOG_URI")) {
166  sentry_dsn = env;
167  LOG(INFO) << "SENTRY_LOG_URI found: " << sentry_dsn;
168  }
169  } else {
170  LOG(INFO) << "--sentry_dsn flag found: " << sentry_dsn;
171  }
172  if (sentry_dsn.empty()) {
173  LOG(INFO) << "No --sentry_dsn or SENTRY_LOG_URI, sentry is disabled";
174  return;
175  }
176  Dsn dsn;
177  CHECK(ParseDsn(sentry_dsn, &dsn)) << "Could not parse " << sentry_dsn;
178 
179  auto ptr = std::make_unique<SentrySink>(std::move(dsn), context);
180  context->AttachCancellable(ptr.get());
181  ptr->WaitTillRun();
182  ptr.release();
183 }
184 
185 } // namespace util