Skip to content

Commit

Permalink
fix(push): encode push labels
Browse files Browse the repository at this point in the history
Fixes #652
  • Loading branch information
gjasny committed Dec 19, 2023
1 parent 83c055e commit 4fb9c23
Show file tree
Hide file tree
Showing 14 changed files with 179 additions and 5 deletions.
3 changes: 3 additions & 0 deletions core/include/prometheus/labels.h
Expand Up @@ -8,4 +8,7 @@ namespace prometheus {
/// \brief Multiple labels and their value.
using Labels = std::map<std::string, std::string>;

/// \brief Single label and its value.
using Label = Labels::value_type;

} // namespace prometheus
14 changes: 14 additions & 0 deletions push/BUILD.bazel
Expand Up @@ -24,6 +24,20 @@ cc_library(
visibility = ["//visibility:public"],
deps = [
"//core",
"//util",
"@com_github_curl//:curl",
],
)

cc_library(
name = "push_internal_headers",
hdrs = glob(
["src/detail/*.h"],
),
strip_include_prefix = "src",
visibility = ["//push/tests:__subpackages__"],
deps = [
"//core",
"//push",
],
)
13 changes: 11 additions & 2 deletions push/CMakeLists.txt
Expand Up @@ -2,9 +2,12 @@
find_package(CURL REQUIRED)

add_library(push
src/curl_wrapper.cc
src/curl_wrapper.h
src/gateway.cc

src/detail/curl_wrapper.cc
src/detail/curl_wrapper.h
src/detail/label_encoder.cc
src/detail/label_encoder.h
)

add_library(${PROJECT_NAME}::push ALIAS push)
Expand All @@ -18,6 +21,7 @@ target_link_libraries(push
PUBLIC
${PROJECT_NAME}::core
PRIVATE
${PROJECT_NAME}::util
Threads::Threads
CURL::libcurl
$<$<AND:$<BOOL:UNIX>,$<NOT:$<BOOL:APPLE>>>:rt>
Expand Down Expand Up @@ -78,5 +82,10 @@ if(GENERATE_PKGCONFIG)
endif()

if(ENABLE_TESTING)
add_library(push_internal_headers INTERFACE)
add_library(${PROJECT_NAME}::push_internal_headers ALIAS push_internal_headers)
target_include_directories(push_internal_headers INTERFACE src)
target_link_libraries(push_internal_headers INTERFACE ${PROJECT_NAME}::push)

add_subdirectory(tests)
endif()
File renamed without changes.
File renamed without changes.
42 changes: 42 additions & 0 deletions push/src/detail/label_encoder.cc
@@ -0,0 +1,42 @@
#include "label_encoder.h"

#include <algorithm>
#include <ostream>

#include "prometheus/detail/base64.h"

namespace prometheus {
namespace detail {

namespace {
// Does this character need encoding like in RFC 3986 section 2.3?
bool needsEncoding(char c) {
if (c >= 'a' && c <= 'z') {
return false;
}
if (c >= 'A' && c <= 'Z') {
return false;
}
if (c >= '0' && c <= '9') {
return false;
}
if (c == '-' || c == '.' || c == '_' || c == '~') {
return false;
}
return true;
}
} // namespace

void encodeLabel(std::ostream& os, const Label& label) {
if (label.second.empty()) {
os << "/" << label.first << "@base64/=";
} else if (std::any_of(label.second.begin(), label.second.end(),
needsEncoding)) {
os << "/" << label.first << "@base64/"
<< detail::base64url_encode(label.second);
} else {
os << "/" << label.first << "/" << label.second;
}
}
} // namespace detail
} // namespace prometheus
13 changes: 13 additions & 0 deletions push/src/detail/label_encoder.h
@@ -0,0 +1,13 @@
#pragma once

#include <iosfwd>

#include "prometheus/labels.h"

namespace prometheus {
namespace detail {

void encodeLabel(std::ostream& os, const Label& label);

}
} // namespace prometheus
8 changes: 5 additions & 3 deletions push/src/gateway.cc
Expand Up @@ -8,7 +8,8 @@
#include <mutex>
#include <sstream>

#include "curl_wrapper.h"
#include "detail/curl_wrapper.h"
#include "detail/label_encoder.h"
#include "prometheus/detail/future_std.h"
#include "prometheus/metric_family.h" // IWYU pragma: keep
#include "prometheus/text_serializer.h"
Expand All @@ -24,12 +25,13 @@ Gateway::Gateway(const std::string& host, const std::string& port,
curlWrapper_ = detail::make_unique<detail::CurlWrapper>(username, password);

std::stringstream jobUriStream;
jobUriStream << host << ':' << port << "/metrics/job/" << jobname;
jobUriStream << host << ':' << port << "/metrics";
detail::encodeLabel(jobUriStream, {"job", jobname});
jobUri_ = jobUriStream.str();

std::stringstream labelStream;
for (auto& label : labels) {
labelStream << "/" << label.first << "/" << label.second;
detail::encodeLabel(labelStream, label);
}
labels_ = labelStream.str();
}
Expand Down
1 change: 1 addition & 0 deletions push/tests/CMakeLists.txt
@@ -1 +1,2 @@
add_subdirectory(integration)
add_subdirectory(internal)
13 changes: 13 additions & 0 deletions push/tests/internal/BUILD.bazel
@@ -0,0 +1,13 @@
cc_test(
name = "internal",
srcs = glob([
"*.cc",
"*.h",
]),
copts = ["-Iexternal/googletest/include"],
linkstatic = True,
deps = [
"//push:push_internal_headers",
"@com_google_googletest//:gtest_main",
],
)
14 changes: 14 additions & 0 deletions push/tests/internal/CMakeLists.txt
@@ -0,0 +1,14 @@
add_executable(prometheus_push_internal_test
label_encoder_test.cc
)

target_link_libraries(prometheus_push_internal_test
PRIVATE
${PROJECT_NAME}::push_internal_headers
GTest::gmock_main
)

add_test(
NAME prometheus_push_internal_test
COMMAND prometheus_push_internal_test
)
43 changes: 43 additions & 0 deletions push/tests/internal/label_encoder_test.cc
@@ -0,0 +1,43 @@
#include "detail/label_encoder.h"

#include <gtest/gtest.h>

#include <string>
#include <sstream>

namespace prometheus {
namespace {

class LabelEncoderTest : public testing::Test {
protected:
std::string Encode(const Label& label) {
std::stringstream ss;
detail::encodeLabel(ss, label);
return ss.str();
}
};

// test cases taken from https://github.com/prometheus/pushgateway#url

TEST_F(LabelEncoderTest, regular) {
EXPECT_EQ("/foo/bar", Encode(Label{"foo", "bar"}));
}

TEST_F(LabelEncoderTest, empty) {
EXPECT_EQ("/first_label@base64/=", Encode(Label{"first_label", ""}));
}

TEST_F(LabelEncoderTest, path) {
EXPECT_EQ("/path@base64/L3Zhci90bXA=", Encode(Label{"path", "/var/tmp"}));
}

TEST_F(LabelEncoderTest, unicode) {
const char unicodeText[] =
"\xce\xa0\xcf\x81\xce\xbf\xce\xbc\xce\xb7\xce\xb8\xce\xb5\xcf\x8d\xcf"
"\x82"; // Προμηθεύς
EXPECT_EQ("/name@base64/zqDPgc6_zrzOt864zrXPjc-C",
Encode(Label{"name", unicodeText}));
}

} // namespace
} // namespace prometheus
12 changes: 12 additions & 0 deletions util/include/prometheus/detail/base64.h
@@ -1,5 +1,6 @@
#pragma once

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
Expand Down Expand Up @@ -66,6 +67,17 @@ inline std::string base64_encode(const std::string& input) {
return encoded;
}

// https://tools.ietf.org/html/rfc4648#section-5
inline std::string base64url_encode(const std::string& input) {
std::string s = base64_encode(input);
std::transform(begin(s), end(s), begin(s), [](char c) {
if (c == '+') return '-';
if (c == '/') return '_';
return c;
});
return s;
}

inline std::string base64_decode(const std::string& input) {
if (input.length() % 4) {
throw std::runtime_error("Invalid base64 length!");
Expand Down
8 changes: 8 additions & 0 deletions util/tests/unit/base64_test.cc
Expand Up @@ -47,6 +47,14 @@ TEST(Base64Test, encodeTest) {
}
}

TEST(Base64Test, encodeUrlTest) {
const char unicodeText[] =
"\xce\xa0\xcf\x81\xce\xbf\xce\xbc\xce\xb7\xce\xb8\xce\xb5\xcf\x8d\xcf"
"\x82"; // Προμηθεύς
std::string encoded = detail::base64url_encode(unicodeText);
EXPECT_EQ("zqDPgc6_zrzOt864zrXPjc-C", encoded);
}

TEST(Base64Test, decodeTest) {
for (const auto& test_case : testVector) {
std::string decoded = detail::base64_decode(test_case.encoded);
Expand Down

0 comments on commit 4fb9c23

Please sign in to comment.