• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

PowerDNS / pdns / 18937558685

30 Oct 2025 10:27AM UTC coverage: 73.04% (+0.03%) from 73.014%
18937558685

Pull #16395

github

web-flow
Merge 3b9be1959 into 9769584db
Pull Request #16395: dnsdist: Add option to use incoming OpenTelemetry Trace ID

38333 of 63186 branches covered (60.67%)

Branch coverage included in aggregate %.

109 of 120 new or added lines in 5 files covered. (90.83%)

64 existing lines in 15 files now uncovered.

127583 of 163972 relevant lines covered (77.81%)

5996675.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.27
/pdns/dnsdistdist/dnsdist-carbon.cc
1
/*
2
 * This file is part of PowerDNS or dnsdist.
3
 * Copyright -- PowerDNS.COM B.V. and its contributors
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of version 2 of the GNU General Public License as
7
 * published by the Free Software Foundation.
8
 *
9
 * In addition, for the avoidance of any doubt, permission is granted to
10
 * link this program with OpenSSL and to (re)distribute the binaries
11
 * produced as the result of such linking.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
 */
22
#ifdef HAVE_CONFIG_H
23
#include "config.h"
24
#endif
25

26
#include "dnsdist-carbon.hh"
27
#include "dnsdist-cache.hh"
28
#include "dnsdist.hh"
29
#include "dnsdist-backoff.hh"
30
#include "dnsdist-configuration.hh"
31
#include "dnsdist-frontend.hh"
32
#include "dnsdist-metrics.hh"
33

34
#ifndef DISABLE_CARBON
35
#include "dolog.hh"
36
#include "sstuff.hh"
37
#include "threadname.hh"
38

39
namespace dnsdist
40
{
41

42
static bool doOneCarbonExport(const Carbon::Endpoint& endpoint)
43
{
8✔
44
  const auto& server = endpoint.server;
8✔
45
  const std::string& namespace_name = endpoint.namespace_name;
8✔
46
  const std::string& hostname = endpoint.ourname;
8✔
47
  const std::string& instance_name = endpoint.instance_name;
8✔
48

49
  try {
8✔
50
    Socket carbonSock(server.sin4.sin_family, SOCK_STREAM);
8✔
51
    carbonSock.setNonBlocking();
8✔
52
    carbonSock.connect(server); // we do the connect so the attempt happens while we gather stats
8✔
53
    ostringstream str;
8✔
54

55
    const time_t now = time(nullptr);
8✔
56

57
    {
8✔
58
      auto entries = dnsdist::metrics::g_stats.entries.read_lock();
8✔
59
      for (const auto& entry : *entries) {
688✔
60
        // Skip non-empty labels, since labels are not supported in Carbon
61
        if (!entry.d_labels.empty()) {
688!
62
          continue;
×
63
        }
×
64

65
        str << namespace_name << "." << hostname << "." << instance_name << "." << entry.d_name << ' ';
688✔
66
        if (const auto& val = std::get_if<pdns::stat_t*>(&entry.d_value)) {
688✔
67
          str << (*val)->load();
335✔
68
        }
335✔
69
        else if (const auto& adval = std::get_if<pdns::stat_double_t*>(&entry.d_value)) {
353✔
70
          str << (*adval)->load();
192✔
71
        }
192✔
72
        else if (const auto& func = std::get_if<dnsdist::metrics::Stats::statfunction_t>(&entry.d_value)) {
161✔
73
          str << (*func)(entry.d_name);
160✔
74
        }
160✔
75
        str << ' ' << now << "\r\n";
688✔
76
      }
688✔
77
    }
8✔
78

79
    for (const auto& state : dnsdist::configuration::getCurrentRuntimeConfiguration().d_backends) {
24✔
80
      string serverName = state->getName().empty() ? state->d_config.remote.toStringWithPort() : state->getName();
24!
81
      std::replace(serverName.begin(), serverName.end(), '.', '_');
24✔
82
      string base = namespace_name;
24✔
83
      base += ".";
24✔
84
      base += hostname;
24✔
85
      base += ".";
24✔
86
      base += instance_name;
24✔
87
      base += ".servers.";
24✔
88
      base += serverName;
24✔
89
      base += ".";
24✔
90
      str << base << "queries" << ' ' << state->queries.load() << " " << now << "\r\n";
24✔
91
      str << base << "responses" << ' ' << state->responses.load() << " " << now << "\r\n";
24✔
92
      str << base << "drops" << ' ' << state->reuseds.load() << " " << now << "\r\n";
24✔
93
      str << base << "latency" << ' ' << (state->d_config.d_availability != DownstreamState::Availability::Down ? state->latencyUsec / 1000.0 : 0) << " " << now << "\r\n";
24✔
94
      str << base << "latencytcp" << ' ' << (state->d_config.d_availability != DownstreamState::Availability::Down ? state->latencyUsecTCP / 1000.0 : 0) << " " << now << "\r\n";
24✔
95
      str << base << "senderrors" << ' ' << state->sendErrors.load() << " " << now << "\r\n";
24✔
96
      str << base << "outstanding" << ' ' << state->outstanding.load() << " " << now << "\r\n";
24✔
97
      str << base << "tcpdiedsendingquery" << ' ' << state->tcpDiedSendingQuery.load() << " " << now << "\r\n";
24✔
98
      str << base << "tcpdiedreaddingresponse" << ' ' << state->tcpDiedReadingResponse.load() << " " << now << "\r\n";
24✔
99
      str << base << "tcpgaveup" << ' ' << state->tcpGaveUp.load() << " " << now << "\r\n";
24✔
100
      str << base << "tcpreadimeouts" << ' ' << state->tcpReadTimeouts.load() << " " << now << "\r\n";
24✔
101
      str << base << "tcpwritetimeouts" << ' ' << state->tcpWriteTimeouts.load() << " " << now << "\r\n";
24✔
102
      str << base << "tcpconnecttimeouts" << ' ' << state->tcpConnectTimeouts.load() << " " << now << "\r\n";
24✔
103
      str << base << "tcpcurrentconnections" << ' ' << state->tcpCurrentConnections.load() << " " << now << "\r\n";
24✔
104
      str << base << "tcpmaxconcurrentconnections" << ' ' << state->tcpMaxConcurrentConnections.load() << " " << now << "\r\n";
24✔
105
      str << base << "tcpnewconnections" << ' ' << state->tcpNewConnections.load() << " " << now << "\r\n";
24✔
106
      str << base << "tcpreusedconnections" << ' ' << state->tcpReusedConnections.load() << " " << now << "\r\n";
24✔
107
      str << base << "tlsresumptions" << ' ' << state->tlsResumptions.load() << " " << now << "\r\n";
24✔
108
      str << base << "tcpavgqueriesperconnection" << ' ' << state->tcpAvgQueriesPerConnection.load() << " " << now << "\r\n";
24✔
109
      str << base << "tcpavgconnectionduration" << ' ' << state->tcpAvgConnectionDuration.load() << " " << now << "\r\n";
24✔
110
      str << base << "tcptoomanyconcurrentconnections" << ' ' << state->tcpTooManyConcurrentConnections.load() << " " << now << "\r\n";
24✔
111
      str << base << "healthcheckfailures" << ' ' << state->d_healthCheckMetrics.d_failures << " " << now << "\r\n";
24✔
112
      str << base << "healthcheckfailuresparsing" << ' ' << state->d_healthCheckMetrics.d_parseErrors << " " << now << "\r\n";
24✔
113
      str << base << "healthcheckfailurestimeout" << ' ' << state->d_healthCheckMetrics.d_timeOuts << " " << now << "\r\n";
24✔
114
      str << base << "healthcheckfailuresnetwork" << ' ' << state->d_healthCheckMetrics.d_networkErrors << " " << now << "\r\n";
24✔
115
      str << base << "healthcheckfailuresmismatch" << ' ' << state->d_healthCheckMetrics.d_mismatchErrors << " " << now << "\r\n";
24✔
116
      str << base << "healthcheckfailuresinvalid" << ' ' << state->d_healthCheckMetrics.d_invalidResponseErrors << " " << now << "\r\n";
24✔
117
      str << base << "healthchecklatency" << ' ' << (state->d_healthCheckLatency / 1000.0) << " " << now << "\r\n";
24✔
118
    }
24✔
119

120
    std::map<std::string, uint64_t> frontendDuplicates;
8✔
121
    for (const auto& front : dnsdist::getFrontends()) {
16✔
122
      if (front->udpFD == -1 && front->tcpFD == -1) {
16!
123
        continue;
×
124
      }
×
125

126
      string frontName = front->local.toStringWithPort() + (front->udpFD >= 0 ? "_udp" : "_tcp");
16✔
127
      std::replace(frontName.begin(), frontName.end(), '.', '_');
16✔
128
      auto dupPair = frontendDuplicates.insert({frontName, 1});
16✔
129
      if (!dupPair.second) {
16!
130
        frontName += "_" + std::to_string(dupPair.first->second);
×
131
        ++(dupPair.first->second);
×
132
      }
×
133

134
      string base = namespace_name;
16✔
135
      base += ".";
16✔
136
      base += hostname;
16✔
137
      base += ".";
16✔
138
      base += instance_name;
16✔
139
      base += ".frontends.";
16✔
140
      base += frontName;
16✔
141
      base += ".";
16✔
142
      str << base << "queries" << ' ' << front->queries.load() << " " << now << "\r\n";
16✔
143
      str << base << "responses" << ' ' << front->responses.load() << " " << now << "\r\n";
16✔
144
      str << base << "tcpdiedreadingquery" << ' ' << front->tcpDiedReadingQuery.load() << " " << now << "\r\n";
16✔
145
      str << base << "tcpdiedsendingresponse" << ' ' << front->tcpDiedSendingResponse.load() << " " << now << "\r\n";
16✔
146
      str << base << "tcpgaveup" << ' ' << front->tcpGaveUp.load() << " " << now << "\r\n";
16✔
147
      str << base << "tcpclienttimeouts" << ' ' << front->tcpClientTimeouts.load() << " " << now << "\r\n";
16✔
148
      str << base << "tcpdownstreamtimeouts" << ' ' << front->tcpDownstreamTimeouts.load() << " " << now << "\r\n";
16✔
149
      str << base << "tcpcurrentconnections" << ' ' << front->tcpCurrentConnections.load() << " " << now << "\r\n";
16✔
150
      str << base << "tcpmaxconcurrentconnections" << ' ' << front->tcpMaxConcurrentConnections.load() << " " << now << "\r\n";
16✔
151
      str << base << "tcpavgqueriesperconnection" << ' ' << front->tcpAvgQueriesPerConnection.load() << " " << now << "\r\n";
16✔
152
      str << base << "tcpavgconnectionduration" << ' ' << front->tcpAvgConnectionDuration.load() << " " << now << "\r\n";
16✔
153
      str << base << "tcpavgreadios" << ' ' << front->tcpAvgIOsPerConnection.load() << " " << now << "\r\n";
16✔
154
      str << base << "tls10-queries" << ' ' << front->tls10queries.load() << " " << now << "\r\n";
16✔
155
      str << base << "tls11-queries" << ' ' << front->tls11queries.load() << " " << now << "\r\n";
16✔
156
      str << base << "tls12-queries" << ' ' << front->tls12queries.load() << " " << now << "\r\n";
16✔
157
      str << base << "tls13-queries" << ' ' << front->tls13queries.load() << " " << now << "\r\n";
16✔
158
      str << base << "tls-unknown-queries" << ' ' << front->tlsUnknownqueries.load() << " " << now << "\r\n";
16✔
159
      str << base << "tlsnewsessions" << ' ' << front->tlsNewSessions.load() << " " << now << "\r\n";
16✔
160
      str << base << "tlsresumptions" << ' ' << front->tlsResumptions.load() << " " << now << "\r\n";
16✔
161
      str << base << "tlsunknownticketkeys" << ' ' << front->tlsUnknownTicketKey.load() << " " << now << "\r\n";
16✔
162
      str << base << "tlsinactiveticketkeys" << ' ' << front->tlsInactiveTicketKey.load() << " " << now << "\r\n";
16✔
163

164
      const TLSErrorCounters* errorCounters = nullptr;
16✔
165
      if (front->tlsFrontend != nullptr) {
16!
166
        errorCounters = &front->tlsFrontend->d_tlsCounters;
×
167
      }
×
168
      else if (front->dohFrontend != nullptr) {
16!
169
        errorCounters = &front->dohFrontend->d_tlsContext->d_tlsCounters;
×
170
      }
×
171
      if (errorCounters != nullptr) {
16!
172
        str << base << "tlsdhkeytoosmall" << ' ' << errorCounters->d_dhKeyTooSmall << " " << now << "\r\n";
×
173
        str << base << "tlsinappropriatefallback" << ' ' << errorCounters->d_inappropriateFallBack << " " << now << "\r\n";
×
174
        str << base << "tlsnosharedcipher" << ' ' << errorCounters->d_noSharedCipher << " " << now << "\r\n";
×
175
        str << base << "tlsunknownciphertype" << ' ' << errorCounters->d_unknownCipherType << " " << now << "\r\n";
×
176
        str << base << "tlsunknownkeyexchangetype" << ' ' << errorCounters->d_unknownKeyExchangeType << " " << now << "\r\n";
×
177
        str << base << "tlsunknownprotocol" << ' ' << errorCounters->d_unknownProtocol << " " << now << "\r\n";
×
178
        str << base << "tlsunsupportedec" << ' ' << errorCounters->d_unsupportedEC << " " << now << "\r\n";
×
179
        str << base << "tlsunsupportedprotocol" << ' ' << errorCounters->d_unsupportedProtocol << " " << now << "\r\n";
×
180
      }
×
181
    }
16✔
182

183
    for (const auto& entry : dnsdist::configuration::getCurrentRuntimeConfiguration().d_pools) {
8✔
184
      string poolName = entry.first;
8✔
185
      std::replace(poolName.begin(), poolName.end(), '.', '_');
8✔
186
      if (poolName.empty()) {
8!
187
        poolName = "_default_";
8✔
188
      }
8✔
189
      string base = namespace_name;
8✔
190
      base += ".";
8✔
191
      base += hostname;
8✔
192
      base += ".";
8✔
193
      base += instance_name;
8✔
194
      base += ".pools.";
8✔
195
      base += poolName;
8✔
196
      base += ".";
8✔
197
      const ServerPool& pool = entry.second;
8✔
198
      str << base << "servers"
8✔
199
          << " " << pool.countServers(false) << " " << now << "\r\n";
8✔
200
      str << base << "servers-up"
8✔
201
          << " " << pool.countServers(true) << " " << now << "\r\n";
8✔
202
      if (pool.packetCache != nullptr) {
8!
203
        const auto& cache = pool.packetCache;
×
204
        str << base << "cache-size"
×
205
            << " " << cache->getMaxEntries() << " " << now << "\r\n";
×
206
        str << base << "cache-entries"
×
207
            << " " << cache->getEntriesCount() << " " << now << "\r\n";
×
208
        str << base << "cache-hits"
×
209
            << " " << cache->getHits() << " " << now << "\r\n";
×
210
        str << base << "cache-misses"
×
211
            << " " << cache->getMisses() << " " << now << "\r\n";
×
212
        str << base << "cache-deferred-inserts"
×
213
            << " " << cache->getDeferredInserts() << " " << now << "\r\n";
×
214
        str << base << "cache-deferred-lookups"
×
215
            << " " << cache->getDeferredLookups() << " " << now << "\r\n";
×
216
        str << base << "cache-lookup-collisions"
×
217
            << " " << cache->getLookupCollisions() << " " << now << "\r\n";
×
218
        str << base << "cache-insert-collisions"
×
219
            << " " << cache->getInsertCollisions() << " " << now << "\r\n";
×
220
        str << base << "cache-ttl-too-shorts"
×
221
            << " " << cache->getTTLTooShorts() << " " << now << "\r\n";
×
222
        str << base << "cache-cleanup-count"
×
223
            << " " << cache->getCleanupCount() << " " << now << "\r\n";
×
224
      }
×
225
    }
8✔
226

227
#ifdef HAVE_DNS_OVER_HTTPS
8✔
228
    {
8✔
229
      std::map<std::string, uint64_t> dohFrontendDuplicates;
8✔
230
      const string base = "dnsdist." + hostname + ".main.doh.";
8✔
231
      for (const auto& doh : dnsdist::getDoHFrontends()) {
8!
232
        string name = doh->d_tlsContext->d_addr.toStringWithPort();
×
233
        std::replace(name.begin(), name.end(), '.', '_');
×
234
        std::replace(name.begin(), name.end(), ':', '_');
×
235
        std::replace(name.begin(), name.end(), '[', '_');
×
236
        std::replace(name.begin(), name.end(), ']', '_');
×
237

238
        auto dupPair = dohFrontendDuplicates.insert({name, 1});
×
239
        if (!dupPair.second) {
×
240
          name += "_" + std::to_string(dupPair.first->second);
×
241
          ++(dupPair.first->second);
×
242
        }
×
243

244
        const vector<pair<const char*, const pdns::stat_t&>> values{
×
245
          {"http-connects", doh->d_httpconnects},
×
246
          {"http1-queries", doh->d_http1Stats.d_nbQueries},
×
247
          {"http2-queries", doh->d_http2Stats.d_nbQueries},
×
248
          {"http1-200-responses", doh->d_http1Stats.d_nb200Responses},
×
249
          {"http2-200-responses", doh->d_http2Stats.d_nb200Responses},
×
250
          {"http1-400-responses", doh->d_http1Stats.d_nb400Responses},
×
251
          {"http2-400-responses", doh->d_http2Stats.d_nb400Responses},
×
252
          {"http1-403-responses", doh->d_http1Stats.d_nb403Responses},
×
253
          {"http2-403-responses", doh->d_http2Stats.d_nb403Responses},
×
254
          {"http1-500-responses", doh->d_http1Stats.d_nb500Responses},
×
255
          {"http2-500-responses", doh->d_http2Stats.d_nb500Responses},
×
256
          {"http1-502-responses", doh->d_http1Stats.d_nb502Responses},
×
257
          {"http2-502-responses", doh->d_http2Stats.d_nb502Responses},
×
258
          {"http1-other-responses", doh->d_http1Stats.d_nbOtherResponses},
×
259
          {"http2-other-responses", doh->d_http2Stats.d_nbOtherResponses},
×
260
          {"get-queries", doh->d_getqueries},
×
261
          {"post-queries", doh->d_postqueries},
×
262
          {"bad-requests", doh->d_badrequests},
×
263
          {"error-responses", doh->d_errorresponses},
×
264
          {"redirect-responses", doh->d_redirectresponses},
×
265
          {"valid-responses", doh->d_validresponses}};
×
266

267
        for (const auto& item : values) {
×
268
          str << base << name << "." << item.first << " " << item.second << " " << now << "\r\n";
×
269
        }
×
270
      }
×
271
    }
8✔
272
#endif /* HAVE_DNS_OVER_HTTPS */
8✔
273

274
    {
8✔
275
      std::string qname;
8✔
276
      auto records = dnsdist::QueryCount::g_queryCountRecords.write_lock();
8✔
277
      for (const auto& record : *records) {
8!
278
        qname = record.first;
×
279
        std::replace(qname.begin(), qname.end(), '.', '_');
×
280
        str << "dnsdist.querycount." << qname << ".queries " << record.second << " " << now << "\r\n";
×
281
      }
×
282
      records->clear();
8✔
283
    }
8✔
284

285
    const string msg = str.str();
8✔
286

287
    int ret = waitForRWData(carbonSock.getHandle(), false, 1, 0);
8✔
288
    if (ret <= 0) {
8!
289
      vinfolog("Unable to write data to carbon server on %s: %s", server.toStringWithPort(), (ret < 0 ? stringerror() : "Timeout"));
×
290
      return false;
×
291
    }
×
292
    carbonSock.setBlocking();
8✔
293
    writen2(carbonSock.getHandle(), msg.c_str(), msg.size());
8✔
294
  }
8✔
295
  catch (const std::exception& e) {
8✔
296
    warnlog("Problem sending carbon data to %s: %s", server.toStringWithPort(), e.what());
×
297
    return false;
×
298
  }
×
299

300
  return true;
8✔
301
}
8✔
302

303
static void carbonHandler(const Carbon::Endpoint& endpoint)
304
{
2✔
305
  setThreadName("dnsdist/carbon");
2✔
306
  const auto intervalUSec = endpoint.interval * 1000 * 1000;
2✔
307
  /* maximum interval between two attempts is 10 minutes */
308
  const ExponentialBackOffTimer backOffTimer(10 * 60);
2✔
309

310
  try {
2✔
311
    uint8_t consecutiveFailures = 0;
2✔
312
    do {
8✔
313
      dnsdist::configuration::refreshLocalRuntimeConfiguration();
8✔
314

315
      DTime dtimer;
8✔
316
      dtimer.set();
8✔
317
      if (doOneCarbonExport(endpoint)) {
8!
318
        const auto elapsedUSec = dtimer.udiff();
8✔
319
        if (elapsedUSec < 0 || static_cast<unsigned int>(elapsedUSec) <= intervalUSec) {
8!
320
          useconds_t toSleepUSec = intervalUSec - elapsedUSec;
8✔
321
          usleep(toSleepUSec);
8✔
322
        }
8✔
UNCOV
323
        else {
×
UNCOV
324
          vinfolog("Carbon export for %s took longer (%s us) than the configured interval (%d us)", endpoint.server.toStringWithPort(), elapsedUSec, intervalUSec);
×
UNCOV
325
        }
×
326
        consecutiveFailures = 0;
8✔
327
      }
8✔
UNCOV
328
      else {
×
UNCOV
329
        const auto backOff = backOffTimer.get(consecutiveFailures);
×
UNCOV
330
        if (consecutiveFailures < std::numeric_limits<decltype(consecutiveFailures)>::max()) {
×
331
          consecutiveFailures++;
×
332
        }
×
UNCOV
333
        vinfolog("Run for %s - %s failed, next attempt in %d", endpoint.server.toStringWithPort(), endpoint.ourname, backOff);
×
UNCOV
334
        std::this_thread::sleep_for(std::chrono::seconds(backOff));
×
UNCOV
335
      }
×
336
    } while (true);
8✔
337
  }
2✔
338
  catch (const PDNSException& e) {
2✔
339
    errlog("Carbon thread for %s died, PDNSException: %s", endpoint.server.toStringWithPort(), e.reason);
×
340
  }
×
341
  catch (...) {
2✔
342
    errlog("Carbon thread for %s died", endpoint.server.toStringWithPort());
×
343
  }
×
344
}
2✔
345

346
Carbon::Endpoint Carbon::newEndpoint(const std::string& address, std::string ourName, uint64_t interval, const std::string& namespace_name, const std::string& instance_name)
347
{
4✔
348
  if (ourName.empty()) {
4!
349
    try {
×
350
      ourName = getCarbonHostName();
×
351
    }
×
352
    catch (const std::exception& exp) {
×
353
      throw std::runtime_error(std::string("The 'ourname' setting in 'carbonServer()' has not been set and we are unable to determine the system's hostname: ") + exp.what());
×
354
    }
×
355
  }
×
356
  return Carbon::Endpoint{ComboAddress(address, 2003),
4✔
357
                          !namespace_name.empty() ? namespace_name : "dnsdist",
4!
358
                          std::move(ourName),
4✔
359
                          !instance_name.empty() ? instance_name : "main",
4!
360
                          interval < std::numeric_limits<unsigned int>::max() ? static_cast<unsigned int>(interval) : 30};
4!
361
}
4✔
362

363
void Carbon::run(const std::vector<Carbon::Endpoint>& endpoints)
364
{
396✔
365
  for (const auto& endpoint : endpoints) {
396✔
366
    std::thread newHandler(carbonHandler, endpoint);
2✔
367
    newHandler.detach();
2✔
368
  }
2✔
369
}
396✔
370

371
}
372
#endif /* DISABLE_CARBON */
373

374
static const time_t s_start = time(nullptr);
375

376
uint64_t uptimeOfProcess(const std::string& str)
377
{
258✔
378
  (void)str;
258✔
379
  return time(nullptr) - s_start;
258✔
380
}
258✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc