Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions tools/server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
set(TARGET server-context)

add_library(${TARGET} STATIC
server-metrics.cpp
server-metrics.h
server-task.cpp
server-task.h
server-queue.cpp
Expand Down Expand Up @@ -71,3 +73,14 @@ target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR})
target_link_libraries(${TARGET} PRIVATE server-context PUBLIC common cpp-httplib ${CMAKE_THREAD_LIBS_INIT})

target_compile_features(${TARGET} PRIVATE cxx_std_17)

if (LLAMA_BUILD_TESTS)
add_executable(test-server-metrics
tests/test-server-metrics.cpp
server-metrics.cpp
)
target_include_directories(test-server-metrics PRIVATE ../mtmd ${CMAKE_SOURCE_DIR})
target_link_libraries(test-server-metrics PRIVATE common)
target_compile_features(test-server-metrics PRIVATE cxx_std_17)
add_test(NAME test-server-metrics COMMAND test-server-metrics)
endif()
18 changes: 14 additions & 4 deletions tools/server/server-context.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "server-context.h"
#include "server-common.h"
#include "server-http.h"
#include "server-metrics.h"
#include "server-task.h"
#include "server-queue.h"

Expand All @@ -21,6 +22,7 @@
#include <exception>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <filesystem>

// fix problem with std::min and std::max
Expand Down Expand Up @@ -3638,10 +3640,18 @@ void server_routes::init_routes() {
const std::string name = metric_def.at("name");
const std::string help = metric_def.at("help");

auto value = json_value(metric_def, "value", 0.);
prometheus << "# HELP llamacpp:" << name << " " << help << "\n"
<< "# TYPE llamacpp:" << name << " " << type << "\n"
<< "llamacpp:" << name << " " << value << "\n";
try {
prometheus << server_prometheus_format_metric(
name, type, help, metric_def.at("value"));
} catch (const std::invalid_argument &) {
res->error(format_error_response(
"Metric value is not a valid finite number", ERROR_TYPE_SERVER));
return res;
} catch (const std::runtime_error &) {
res->error(format_error_response(
"Metric value could not be serialized", ERROR_TYPE_SERVER));
return res;
}
}
}

Expand Down
40 changes: 40 additions & 0 deletions tools/server/server-metrics.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "server-metrics.h"

#include <cmath>
#include <iomanip>
#include <limits>
#include <locale>
#include <sstream>
#include <stdexcept>

std::string server_prometheus_format_metric(
const std::string & name,
const std::string & type,
const std::string & help,
const json & value) {
std::string numeric_value;
if (value.is_number_unsigned()) {
numeric_value = std::to_string(value.get<uint64_t>());
} else if (value.is_number_integer()) {
numeric_value = std::to_string(value.get<int64_t>());
} else if (value.is_number_float()) {
const double number = value.get<double>();
if (!std::isfinite(number)) {
throw std::invalid_argument("Prometheus metric value is not finite");
}
std::ostringstream stream;
stream.imbue(std::locale::classic());
stream << std::setprecision(std::numeric_limits<double>::max_digits10) << number;
if (!stream) {
throw std::runtime_error("Prometheus metric value could not be formatted");
}
numeric_value = stream.str();
} else {
throw std::invalid_argument("Prometheus metric value is not numeric");
}

const std::string full_name = "llamacpp:" + name;
return "# HELP " + full_name + " " + help + "\n"
+ "# TYPE " + full_name + " " + type + "\n"
+ full_name + " " + numeric_value + "\n";
}
14 changes: 14 additions & 0 deletions tools/server/server-metrics.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include "server-common.h"

#include <string>

// Format one complete Prometheus metric. Throws std::invalid_argument when
// value is not a supported finite numeric JSON value, or std::runtime_error
// if the value cannot be formatted.
std::string server_prometheus_format_metric(
const std::string & name,
const std::string & type,
const std::string & help,
const json & value);
139 changes: 139 additions & 0 deletions tools/server/tests/test-server-metrics.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#include "server-metrics.h"

#include <cmath>
#include <cstdlib>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <string>
#include <vector>

static void expect_equal(const std::string & expected, const std::string & actual) {
if (expected != actual) {
std::cerr << "expected:\n" << expected << "actual:\n" << actual;
throw std::runtime_error("Prometheus metric formatting mismatch");
}
}

static void expect_integer(const json & value, const std::string & expected) {
expect_equal(
"# HELP llamacpp:test_metric Test metric.\n"
"# TYPE llamacpp:test_metric counter\n"
"llamacpp:test_metric " + expected + "\n",
server_prometheus_format_metric("test_metric", "counter", "Test metric.", value));
}

static void expect_float_round_trip(double value) {
const std::string output = server_prometheus_format_metric(
"test_gauge", "gauge", "Test gauge.", json(value));
const std::string prefix = "llamacpp:test_gauge ";
const size_t sample = output.rfind(prefix);
if (sample == std::string::npos || output.back() != '\n') {
throw std::runtime_error("Prometheus sample line is unavailable");
}
const std::string formatted = output.substr(
sample + prefix.size(), output.size() - sample - prefix.size() - 1);
char * end = nullptr;
const double reparsed = std::strtod(formatted.c_str(), &end);
if (end != formatted.c_str() + formatted.size() || reparsed != value) {
throw std::runtime_error("floating metric did not round trip");
}
}

static void expect_invalid(const json & value) {
try {
(void) server_prometheus_format_metric("invalid", "gauge", "Invalid metric.", value);
} catch (const std::invalid_argument &) {
return;
}
throw std::runtime_error("invalid Prometheus metric value was accepted");
}

static void test_endpoint_layout_and_regression_values() {
expect_equal(
"# HELP llamacpp:prompt_tokens_total Number of prompt tokens processed.\n"
"# TYPE llamacpp:prompt_tokens_total counter\n"
"llamacpp:prompt_tokens_total 1423152\n",
server_prometheus_format_metric(
"prompt_tokens_total",
"counter",
"Number of prompt tokens processed.",
json(uint64_t(1423152))));
expect_equal(
"# HELP llamacpp:tokens_predicted_total Number of generation tokens processed.\n"
"# TYPE llamacpp:tokens_predicted_total counter\n"
"llamacpp:tokens_predicted_total 180\n",
server_prometheus_format_metric(
"tokens_predicted_total",
"counter",
"Number of generation tokens processed.",
json(uint64_t(180))));
}

static void test_integer_boundaries() {
expect_integer(json(uint64_t(0)), "0");
expect_integer(json(uint64_t(999999)), "999999");
expect_integer(json(uint64_t(1000000)), "1000000");
expect_integer(json(uint64_t(1030220)), "1030220");
expect_integer(json(uint64_t(1423152)), "1423152");
expect_integer(json(uint64_t(9007199254740991ULL)), "9007199254740991");
expect_integer(json(uint64_t(9007199254740992ULL)), "9007199254740992");
expect_integer(json(uint64_t(9007199254740993ULL)), "9007199254740993");
expect_integer(json(std::numeric_limits<uint64_t>::max()), "18446744073709551615");
expect_integer(json(int64_t(-42)), "-42");
expect_integer(json(std::numeric_limits<int64_t>::min()), "-9223372036854775808");
expect_integer(json(std::numeric_limits<int64_t>::max()), "9223372036854775807");
}

static void test_floating_gauges() {
expect_equal(
"# HELP llamacpp:fractional_gauge Fractional test gauge.\n"
"# TYPE llamacpp:fractional_gauge gauge\n"
"llamacpp:fractional_gauge 1.0000000000000002\n",
server_prometheus_format_metric(
"fractional_gauge",
"gauge",
"Fractional test gauge.",
json(1.0000000000000002)));
for (double value : std::vector<double> {
0.125,
-12345.678901234567,
std::numeric_limits<double>::denorm_min(),
std::numeric_limits<double>::max(),
}) {
expect_float_round_trip(value);
}
}

static void test_json_number_kinds_remain_distinct() {
expect_integer(json(uint64_t(9007199254740993ULL)), "9007199254740993");
expect_equal(
"# HELP llamacpp:test_metric Test metric.\n"
"# TYPE llamacpp:test_metric gauge\n"
"llamacpp:test_metric 9007199254740992\n",
server_prometheus_format_metric(
"test_metric",
"gauge",
"Test metric.",
json(static_cast<double>(9007199254740993ULL))));
}

static void test_invalid_values() {
expect_invalid(json(nullptr));
expect_invalid(json(true));
expect_invalid(json("1423152"));
expect_invalid(json::array({1}));
expect_invalid(json::object({{"value", 1}}));
expect_invalid(json(std::numeric_limits<double>::quiet_NaN()));
expect_invalid(json(std::numeric_limits<double>::infinity()));
expect_invalid(json(-std::numeric_limits<double>::infinity()));
}

int main() {
test_endpoint_layout_and_regression_values();
test_integer_boundaries();
test_floating_gauges();
test_json_number_kinds_remain_distinct();
test_invalid_values();
return 0;
}