Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stats: Capture all the strings used to accumulate http stats in an object, plumbed through the system. #4997

Merged
merged 35 commits into from
Dec 5, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d70cb18
Make a class for Http::CodeUtility::chargeResponseTiming et al, plumb…
jmarantz Nov 8, 2018
9c49dec
Use join() rather than fmt::format, and hold static strings in string…
jmarantz Nov 8, 2018
df7d7a1
fix router test (empty prefix) and use const string_view for member v…
jmarantz Nov 8, 2018
da648b3
add speed-test.
jmarantz Nov 8, 2018
ec66aaf
Remove libraries not needed by speed test.
jmarantz Nov 8, 2018
d509154
Merge branch 'master' into http-code-stats-as-object
jmarantz Nov 9, 2018
cf7565d
Merge branch 'master' into http-code-stats-as-object
jmarantz Nov 16, 2018
9cd564e
Merge branch 'master' into http-code-stats-as-object
jmarantz Nov 21, 2018
255c51b
format
jmarantz Nov 21, 2018
cca3bc9
Move the CodeStats instance out of the server and into listener_manag…
jmarantz Nov 25, 2018
bdea1d7
Add HTTP Context object.
jmarantz Nov 26, 2018
e7d59b2
Merge branch 'master' into http-code-stats-as-object
jmarantz Nov 26, 2018
60d7454
Merge branch 'master' into htp-context
jmarantz Nov 27, 2018
eb0be50
mostly working; need to commit to sync.
jmarantz Nov 27, 2018
60c4a73
Merge branch 'master' into htp-context
jmarantz Nov 27, 2018
4edb9cd
all tests building; one failure
jmarantz Nov 27, 2018
b8fc8da
all tests working.
jmarantz Nov 27, 2018
228c52d
Merge branch 'htp-context' into http-code-stats-as-object
jmarantz Nov 27, 2018
c257d69
Clean up comments and add tests.
jmarantz Nov 28, 2018
2cc92ec
clang-tidy fixes.
jmarantz Nov 28, 2018
aea8630
Add comments to internal function descriptions.
jmarantz Nov 29, 2018
30e0c15
Merge branch 'master' into http-code-stats-as-object
jmarantz Nov 29, 2018
47e014e
Merge branch 'master' into http-code-stats-as-object
jmarantz Dec 2, 2018
21e5a34
Update comment and remove empty ctors/dtors from cc files.
jmarantz Dec 2, 2018
3a2cef0
Remove superfluous include.
jmarantz Dec 2, 2018
dcf800b
Back out the changes to make config_ a direct object in server objects.
jmarantz Dec 2, 2018
72e6dd3
Contain ConfigurationImpl directly in the servers, simplifying a few …
jmarantz Dec 2, 2018
522e235
Fix dereference.
jmarantz Dec 2, 2018
01bd77a
all tests working.
jmarantz Dec 2, 2018
7802653
Merge branch 'master' into config-impl-contained
jmarantz Dec 3, 2018
c3508fa
Reword comment.
jmarantz Dec 3, 2018
0c8ac51
Merge branch 'config-impl-contained' into http-code-stats-as-object
jmarantz Dec 3, 2018
4b32611
Merge branch 'master' into http-code-stats-as-object
jmarantz Dec 3, 2018
4f0ebb5
Remove commented-out code.
jmarantz Dec 3, 2018
d08fc60
attempt to fix clang-tidy errors.
jmarantz Dec 4, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/envoy/http/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ envoy_cc_library(
envoy_cc_library(
name = "codes_interface",
hdrs = ["codes.h"],
deps = ["//include/envoy/stats:stats_interface"],
)

envoy_cc_library(
Expand Down
53 changes: 53 additions & 0 deletions include/envoy/http/codes.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#pragma once

#include <chrono>

#include "envoy/stats/scope.h"

namespace Envoy {
namespace Http {

Expand Down Expand Up @@ -73,5 +77,54 @@ enum class Code {
// clang-format on
};

class CodeStats {
public:
virtual ~CodeStats() {}

struct ResponseStatInfo {
Stats::Scope& global_scope_;
Stats::Scope& cluster_scope_;
const std::string& prefix_;
uint64_t response_status_code_;
bool internal_request_;
const std::string& request_vhost_name_;
const std::string& request_vcluster_name_;
const std::string& from_zone_;
const std::string& to_zone_;
bool upstream_canary_;
};

struct ResponseTimingInfo {
Stats::Scope& global_scope_;
Stats::Scope& cluster_scope_;
const std::string& prefix_;
std::chrono::milliseconds response_time_;
bool upstream_canary_;
bool internal_request_;
const std::string& request_vhost_name_;
const std::string& request_vcluster_name_;
const std::string& from_zone_;
const std::string& to_zone_;
};

/**
* Charge a simple response stat to an upstream.
*/
virtual void chargeBasicResponseStat(Stats::Scope& scope, const std::string& prefix,
Code response_code) PURE;

/**
* Charge a response stat to both agg counters (*xx) as well as code specific counters. This
* routine also looks for the x-envoy-upstream-canary header and if it is set, also charges
* canary stats.
*/
virtual void chargeResponseStat(const ResponseStatInfo& info) PURE;

/**
* Charge a response timing to the various dynamic stat postfixes.
*/
virtual void chargeResponseTiming(const ResponseTimingInfo& info) PURE;
};

} // namespace Http
} // namespace Envoy
1 change: 1 addition & 0 deletions include/envoy/server/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ envoy_cc_library(
deps = [
":admin_interface",
"//include/envoy/access_log:access_log_interface",
"//include/envoy/http:codes_interface",
"//include/envoy/http:filter_interface",
"//include/envoy/init:init_interface",
"//include/envoy/json:json_object_interface",
Expand Down
7 changes: 7 additions & 0 deletions include/envoy/server/filter_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "envoy/access_log/access_log.h"
#include "envoy/api/v2/core/base.pb.h"
#include "envoy/http/codes.h"
#include "envoy/http/filter.h"
#include "envoy/init/init.h"
#include "envoy/json/json_object.h"
Expand Down Expand Up @@ -140,6 +141,12 @@ class FactoryContext {
* @return OverloadManager& the overload manager for the server.
*/
virtual OverloadManager& overloadManager() PURE;

/**
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra newline

* @return Http::CodeStats& a reference to the code stats.
*/
virtual Http::CodeStats& codeStats() PURE;
};

class ListenerFactoryContext : public FactoryContext {
Expand Down
5 changes: 5 additions & 0 deletions include/envoy/server/instance.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ class Instance {
* @return the flush interval of stats sinks.
*/
virtual std::chrono::milliseconds statsFlushInterval() const PURE;

/**
* @return Http::CodeStats the http response-code stats
*/
virtual Http::CodeStats& codeStats() PURE;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not crazy about this being a server-wide thing and then having to be plumbed through everywhere. What's the reasoning for that? Can it potentially be per-HCM and then just exposed via HTTP filter callbacks? If you want it to be global can we create it in the singleton manager as part of the HCM (like the HTTP date provider)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HCM==HttpConnectionManager I assume? How many HCMs are there? One per thread?

The reasoning for having it be global is that it's not going to be free to create or store, once SymbolTables are integrated. But if there were a bounded number created at startup it would be OK. There's no compelling reason there has to be only one, as long as they are not created on demand in the hot-path.

What are the drawbacks having them be plumbed everywhere? In my mind the most painful thing is the number of many edits that required. However, I'd be up for a refactor -- even in advance of this PR. One thought I had was making Server::Configuration::FactoryContext more broadly available, and that would make TimeSource, Http::CodeStats, SymbolTable, and whatever else was needed easier to get.

I hadn't really looked at HCM before but it looks like this is a way to pass around data that's derived from configuration, whereas Server::Configuration::FactoryContext looks more like it's sharing common data structures created by the server at startup, which feels like what this is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the drawbacks having them be plumbed everywhere?

Because IMO it's an abstraction leakage. Envoy is not a dedicated HTTP server (though yes we always have the admin server) so it seems incorrect to create this thing and pass it around everywhere when it only applies to HTTP traffic.

IMO we can create one of these things per HCM (it's not per-thread, it's per listener effectively) or if we want one for all listeners you can create a singleton like here: https://github.com/envoyproxy/envoy/blob/master/source/extensions/filters/network/http_connection_manager/config.cc#L66

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough -- individual HTTP-specific objects shouldn't be plumbed through where they are not needed. And using a singleton owned by a per-connection HTTP factory seems workable and I'll go with that for this PR. IIRC you told me once that pattern of singleton-management gets cleaned up between unit tests.

But it does seem like there's a few different http-specific objects that want to be once per server...what do you think of having a class to hold and/or create those (HttpContext or HttpFactory or similar), and have that passed into the HCM constructor? This would effectively be like the singletons but a little cleaner from a statics perspective. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The singleton manager avoids statics, it's an actual object. Take a look at the implementation.

Copy link
Contributor Author

@jmarantz jmarantz Nov 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you're going to be much happier with the change I just made; I was trying to move stuff around for a cleaner dependency graph but I think it was better before I did this.

I just did a commit to see how it looks via the GH interface. I'll follow up further.

Copy link
Contributor Author

@jmarantz jmarantz Nov 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per DM on slack I am adding a new Http::Context object to hold the httpTracer and Http::CodeStats.

The singleton-manager didn't work out due to same-thread assertions.

};

} // namespace Server
Expand Down
3 changes: 2 additions & 1 deletion include/envoy/upstream/cluster_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ class ClusterManagerFactory {
clusterManagerFromProto(const envoy::config::bootstrap::v2::Bootstrap& bootstrap,
Stats::Store& stats, ThreadLocal::Instance& tls, Runtime::Loader& runtime,
Runtime::RandomGenerator& random, const LocalInfo::LocalInfo& local_info,
AccessLog::AccessLogManager& log_manager, Server::Admin& admin) PURE;
AccessLog::AccessLogManager& log_manager, Server::Admin& admin,
Http::CodeStats& code_stats) PURE;

/**
* Allocate an HTTP connection pool for the host. Pools are separated by 'priority',
Expand Down
5 changes: 3 additions & 2 deletions source/common/http/async_client_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ AsyncClientImpl::AsyncClientImpl(Upstream::ClusterInfoConstSharedPtr cluster,
const LocalInfo::LocalInfo& local_info,
Upstream::ClusterManager& cm, Runtime::Loader& runtime,
Runtime::RandomGenerator& random,
Router::ShadowWriterPtr&& shadow_writer)
Router::ShadowWriterPtr&& shadow_writer,
Http::CodeStats& code_stats)
: cluster_(cluster),
config_("http.async-client.", local_info, stats_store, cm, runtime, random,
std::move(shadow_writer), true, false, false, dispatcher.timeSystem()),
std::move(shadow_writer), true, false, false, dispatcher.timeSystem(), code_stats),
dispatcher_(dispatcher) {}

AsyncClientImpl::~AsyncClientImpl() {
Expand Down
3 changes: 2 additions & 1 deletion source/common/http/async_client_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class AsyncClientImpl final : public AsyncClient {
AsyncClientImpl(Upstream::ClusterInfoConstSharedPtr cluster, Stats::Store& stats_store,
Event::Dispatcher& dispatcher, const LocalInfo::LocalInfo& local_info,
Upstream::ClusterManager& cm, Runtime::Loader& runtime,
Runtime::RandomGenerator& random, Router::ShadowWriterPtr&& shadow_writer);
Runtime::RandomGenerator& random, Router::ShadowWriterPtr&& shadow_writer,
Http::CodeStats& code_stats);
~AsyncClientImpl();

// Http::AsyncClient
Expand Down
109 changes: 65 additions & 44 deletions source/common/http/codes.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,124 +7,145 @@
#include "envoy/stats/scope.h"

#include "common/common/enum_to_int.h"
#include "common/common/fmt.h"
#include "common/common/utility.h"
#include "common/http/headers.h"
#include "common/http/utility.h"

#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"

namespace Envoy {
namespace Http {

void CodeUtility::chargeBasicResponseStat(Stats::Scope& scope, const std::string& prefix,
Code response_code) {
CodeStatsImpl::CodeStatsImpl() {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: del empty constuctor/destructor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on past experience I think we can improve the compile/link times for Envoy if we have explicit non-inlined destructors for all virtual classes. But injecting them incrementally in a few PRs without broader evangelism isn't going to move the needle.

The Chromium style guide has similar observations:

https://www.chromium.org/developers/coding-style/cpp-dos-and-donts#TOC-Stop-inlining-constructors-and-destructors

IMO the destructors are more important to leave outlined than the constructors, though, as I think the constructors will only cost you compile/link time when they are actually instantiated, whereas inlined destructors cost you compile-link speed every time they are referenced.

For now though I'll just inline them to match current style.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added #5182 for discussing inline dtors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, I don't really care as long as we are generally consistent. We already do this for mocks for the same reason. I will stop commenting on this.


CodeStatsImpl::~CodeStatsImpl() {}

void CodeStatsImpl::chargeBasicResponseStat(Stats::Scope& scope, const std::string& prefix,
Code response_code) {
// Build a dynamic stat for the response code and increment it.
scope.counter(fmt::format("{}upstream_rq_completed", prefix)).inc();
scope.counter(fmt::format("{}upstream_rq_{}", prefix, groupStringForResponseCode(response_code)))
scope.counter(absl::StrCat(prefix, upstream_rq_completed_)).inc();
scope
.counter(absl::StrCat(prefix, upstream_rq_,
CodeUtility::groupStringForResponseCode(response_code)))
.inc();
scope.counter(fmt::format("{}upstream_rq_{}", prefix, enumToInt(response_code))).inc();
scope.counter(absl::StrCat(prefix, upstream_rq_, enumToInt(response_code))).inc();
}

void CodeUtility::chargeResponseStat(const ResponseStatInfo& info) {
void CodeStatsImpl::chargeResponseStat(const ResponseStatInfo& info) {
const uint64_t response_code = info.response_status_code_;
chargeBasicResponseStat(info.cluster_scope_, info.prefix_, static_cast<Code>(response_code));

std::string group_string = groupStringForResponseCode(static_cast<Code>(response_code));
std::string group_string =
CodeUtility::groupStringForResponseCode(static_cast<Code>(response_code));

// If the response is from a canary, also create canary stats.
if (info.upstream_canary_) {
info.cluster_scope_.counter(fmt::format("{}canary.upstream_rq_completed", info.prefix_)).inc();
info.cluster_scope_.counter(fmt::format("{}canary.upstream_rq_{}", info.prefix_, group_string))
info.cluster_scope_.counter(absl::StrCat(info.prefix_, canary_upstream_rq_completed_)).inc();
info.cluster_scope_.counter(absl::StrCat(info.prefix_, canary_upstream_rq_, group_string))
.inc();
info.cluster_scope_.counter(fmt::format("{}canary.upstream_rq_{}", info.prefix_, response_code))
info.cluster_scope_.counter(absl::StrCat(info.prefix_, canary_upstream_rq_, response_code))
.inc();
}

// Split stats into external vs. internal.
if (info.internal_request_) {
info.cluster_scope_.counter(fmt::format("{}internal.upstream_rq_completed", info.prefix_))
.inc();
info.cluster_scope_
.counter(fmt::format("{}internal.upstream_rq_{}", info.prefix_, group_string))
info.cluster_scope_.counter(absl::StrCat(info.prefix_, internal_upstream_rq_completed_)).inc();
info.cluster_scope_.counter(absl::StrCat(info.prefix_, internal_upstream_rq_, group_string))
.inc();
info.cluster_scope_
.counter(fmt::format("{}internal.upstream_rq_{}", info.prefix_, response_code))
info.cluster_scope_.counter(absl::StrCat(info.prefix_, internal_upstream_rq_, response_code))
.inc();
} else {
info.cluster_scope_.counter(fmt::format("{}external.upstream_rq_completed", info.prefix_))
info.cluster_scope_.counter(absl::StrCat(info.prefix_, external_upstream_rq_completed_)).inc();
info.cluster_scope_.counter(absl::StrCat(info.prefix_, external_upstream_rq_, group_string))
.inc();
info.cluster_scope_
.counter(fmt::format("{}external.upstream_rq_{}", info.prefix_, group_string))
.inc();
info.cluster_scope_
.counter(fmt::format("{}external.upstream_rq_{}", info.prefix_, response_code))
info.cluster_scope_.counter(absl::StrCat(info.prefix_, external_upstream_rq_, response_code))
.inc();
}

// Handle request virtual cluster.
if (!info.request_vcluster_name_.empty()) {
info.global_scope_
.counter(fmt::format("vhost.{}.vcluster.{}.upstream_rq_completed", info.request_vhost_name_,
info.request_vcluster_name_))
.counter(join({vhost_, info.request_vhost_name_, vcluster_, info.request_vcluster_name_,
upstream_rq_completed_}))
.inc();
info.global_scope_
.counter(fmt::format("vhost.{}.vcluster.{}.upstream_rq_{}", info.request_vhost_name_,
info.request_vcluster_name_, group_string))
.counter(join({vhost_, info.request_vhost_name_, vcluster_, info.request_vcluster_name_,
absl::StrCat(upstream_rq_, group_string)}))
.inc();
info.global_scope_
.counter(fmt::format("vhost.{}.vcluster.{}.upstream_rq_{}", info.request_vhost_name_,
info.request_vcluster_name_, response_code))
.counter(join({vhost_, info.request_vhost_name_, vcluster_, info.request_vcluster_name_,
absl::StrCat(upstream_rq_, response_code)}))
.inc();
}

// Handle per zone stats.
if (!info.from_zone_.empty() && !info.to_zone_.empty()) {
absl::string_view prefix_without_trailing_dot = stripTrailingDot(info.prefix_);
info.cluster_scope_
.counter(fmt::format("{}zone.{}.{}.upstream_rq_completed", info.prefix_, info.from_zone_,
info.to_zone_))
.counter(join({prefix_without_trailing_dot, zone_, info.from_zone_, info.to_zone_,
upstream_rq_completed_}))
.inc();
info.cluster_scope_
.counter(fmt::format("{}zone.{}.{}.upstream_rq_{}", info.prefix_, info.from_zone_,
info.to_zone_, group_string))
.counter(join({prefix_without_trailing_dot, zone_, info.from_zone_, info.to_zone_,
absl::StrCat(upstream_rq_, group_string)}))
.inc();
info.cluster_scope_
.counter(fmt::format("{}zone.{}.{}.upstream_rq_{}", info.prefix_, info.from_zone_,
info.to_zone_, response_code))
.counter(join({prefix_without_trailing_dot, zone_, info.from_zone_, info.to_zone_,
absl::StrCat(upstream_rq_, response_code)}))
.inc();
}
}

void CodeUtility::chargeResponseTiming(const ResponseTimingInfo& info) {
info.cluster_scope_.histogram(info.prefix_ + "upstream_rq_time")
void CodeStatsImpl::chargeResponseTiming(const ResponseTimingInfo& info) {
info.cluster_scope_.histogram(absl::StrCat(info.prefix_, upstream_rq_time_))
.recordValue(info.response_time_.count());
if (info.upstream_canary_) {
info.cluster_scope_.histogram(info.prefix_ + "canary.upstream_rq_time")
info.cluster_scope_.histogram(absl::StrCat(info.prefix_, canary_upstream_rq_time_))
.recordValue(info.response_time_.count());
}

if (info.internal_request_) {
info.cluster_scope_.histogram(info.prefix_ + "internal.upstream_rq_time")
info.cluster_scope_.histogram(absl::StrCat(info.prefix_, internal_upstream_rq_time_))
.recordValue(info.response_time_.count());
} else {
info.cluster_scope_.histogram(info.prefix_ + "external.upstream_rq_time")
info.cluster_scope_.histogram(absl::StrCat(info.prefix_, external_upstream_rq_time_))
.recordValue(info.response_time_.count());
}

if (!info.request_vcluster_name_.empty()) {
info.global_scope_
.histogram("vhost." + info.request_vhost_name_ + ".vcluster." +
info.request_vcluster_name_ + ".upstream_rq_time")
.histogram(join({vhost_, info.request_vhost_name_, vcluster_, info.request_vcluster_name_,
upstream_rq_time_}))
.recordValue(info.response_time_.count());
}

// Handle per zone stats.
if (!info.from_zone_.empty() && !info.to_zone_.empty()) {
info.cluster_scope_
.histogram(fmt::format("{}zone.{}.{}.upstream_rq_time", info.prefix_, info.from_zone_,
info.to_zone_))
.histogram(join({stripTrailingDot(info.prefix_), zone_, info.from_zone_, info.to_zone_,
upstream_rq_time_}))
.recordValue(info.response_time_.count());
}
}

absl::string_view CodeStatsImpl::stripTrailingDot(absl::string_view str) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about moving this to StringUtil with trailing char as argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of that...but I'm not sure if this would be useful anywhere else, so I thought maybe it doesn't make that much sense to broaden the interface everyone has to look through.

If you think there might be another use-case I can broaden it.

And actually the only reason to have this function to to avoid removing all the cases of "prefix." throughout the code in this PR. I think instead I'll put a TODO to remove the function and the need for it.

if (absl::EndsWith(str, ".")) {
str.remove_suffix(1);
}
return str;
}

std::string CodeStatsImpl::join(const std::vector<absl::string_view>& v) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you probably use this function here?

static std::string join(const std::vector<std::string>& source, const std::string& delimiter);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No...this one is special; it skips the first token if it's empty. I could name it something different but I couldn't think of anything really brief.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Got it.

ASSERT(!v.empty());
auto iter = v.begin();
if (iter->empty()) {
++iter; // Skip any initial empty prefix.
}
return absl::StrJoin(iter, v.end(), ".");
}

std::string CodeUtility::groupStringForResponseCode(Code response_code) {
if (CodeUtility::is2xx(enumToInt(response_code))) {
return "2xx";
Expand Down
Loading