mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 21:35:42 +00:00
Add runtime controls for Shardok latency tracing
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
|
||||
namespace net::eagle0::common {
|
||||
@@ -15,19 +16,56 @@ auto ReadEnvEnabled() -> bool {
|
||||
return text == "1" || text == "true" || text == "TRUE" || text == "yes" || text == "YES";
|
||||
}
|
||||
|
||||
auto TraceEnabled() -> bool {
|
||||
static const bool enabled = ReadEnvEnabled();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
auto ReadGameFilter() -> std::string {
|
||||
const char* value = std::getenv("EAGLE0_SHARDOK_LATENCY_GAME_ID");
|
||||
return value == nullptr ? "" : std::string(value);
|
||||
}
|
||||
|
||||
auto GameFilter() -> const std::string& {
|
||||
static const std::string filter = ReadGameFilter();
|
||||
return filter;
|
||||
struct ActiveTraceConfig {
|
||||
bool enabled = false;
|
||||
std::string gameId;
|
||||
std::chrono::seconds ttl = std::chrono::seconds::zero();
|
||||
double slowSpanThresholdMs = 0.0;
|
||||
std::optional<std::chrono::steady_clock::time_point> expiresAt;
|
||||
};
|
||||
|
||||
auto ToActiveConfig(const ShardokLatencyTraceConfig& config) -> ActiveTraceConfig {
|
||||
return ActiveTraceConfig{
|
||||
.enabled = config.enabled,
|
||||
.gameId = config.gameId,
|
||||
.ttl = config.ttl,
|
||||
.slowSpanThresholdMs = config.slowSpanThresholdMs,
|
||||
.expiresAt = config.enabled && config.ttl > std::chrono::seconds::zero()
|
||||
? std::optional(std::chrono::steady_clock::now() + config.ttl)
|
||||
: std::nullopt};
|
||||
}
|
||||
|
||||
auto ConfigFromEnv() -> ActiveTraceConfig {
|
||||
return ToActiveConfig(ShardokLatencyTraceConfig{
|
||||
.enabled = ReadEnvEnabled(),
|
||||
.gameId = ReadGameFilter(),
|
||||
.ttl = std::chrono::seconds::zero(),
|
||||
.slowSpanThresholdMs = 0.0});
|
||||
}
|
||||
|
||||
auto ConfigMutex() -> std::mutex& {
|
||||
static auto mutex = new std::mutex();
|
||||
return *mutex;
|
||||
}
|
||||
|
||||
auto Config() -> ActiveTraceConfig& {
|
||||
static auto config = new ActiveTraceConfig(ConfigFromEnv());
|
||||
return *config;
|
||||
}
|
||||
|
||||
auto CurrentConfig() -> ActiveTraceConfig {
|
||||
std::scoped_lock lock(ConfigMutex());
|
||||
auto& config = Config();
|
||||
if (config.expiresAt.has_value() && std::chrono::steady_clock::now() >= *config.expiresAt) {
|
||||
config.enabled = false;
|
||||
config.expiresAt = std::nullopt;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
auto JsonEscape(std::string_view value) -> std::string {
|
||||
@@ -52,10 +90,23 @@ auto LogMutex() -> std::mutex& {
|
||||
|
||||
} // namespace
|
||||
|
||||
void SetShardokLatencyTraceConfig(const ShardokLatencyTraceConfig& config) {
|
||||
std::scoped_lock lock(ConfigMutex());
|
||||
Config() = ToActiveConfig(config);
|
||||
}
|
||||
|
||||
auto GetShardokLatencyTraceConfig() -> ShardokLatencyTraceConfig {
|
||||
const ActiveTraceConfig config = CurrentConfig();
|
||||
return ShardokLatencyTraceConfig{
|
||||
.enabled = config.enabled,
|
||||
.gameId = config.gameId,
|
||||
.ttl = config.ttl,
|
||||
.slowSpanThresholdMs = config.slowSpanThresholdMs};
|
||||
}
|
||||
|
||||
auto ShardokLatencyTraceEnabledForGame(std::string_view gameId) -> bool {
|
||||
if (!TraceEnabled()) { return false; }
|
||||
const std::string& filter = GameFilter();
|
||||
return filter.empty() || filter == gameId;
|
||||
const ActiveTraceConfig config = CurrentConfig();
|
||||
return config.enabled && (config.gameId.empty() || config.gameId == gameId);
|
||||
}
|
||||
|
||||
void EmitShardokLatencyTrace(
|
||||
@@ -63,10 +114,12 @@ void EmitShardokLatencyTrace(
|
||||
std::string_view gameId,
|
||||
const ShardokLatencyFields& fields,
|
||||
std::chrono::steady_clock::duration duration) {
|
||||
if (!ShardokLatencyTraceEnabledForGame(gameId)) { return; }
|
||||
const ActiveTraceConfig config = CurrentConfig();
|
||||
if (!config.enabled || (!config.gameId.empty() && config.gameId != gameId)) { return; }
|
||||
|
||||
const auto durationMs =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(duration).count() / 1000.0;
|
||||
if (config.slowSpanThresholdMs > 0.0 && durationMs < config.slowSpanThresholdMs) { return; }
|
||||
|
||||
std::ostringstream out;
|
||||
out << "{\"event\":\"shardok_latency\","
|
||||
|
||||
@@ -11,6 +11,16 @@ namespace net::eagle0::common {
|
||||
|
||||
using ShardokLatencyFields = std::vector<std::pair<std::string, std::string>>;
|
||||
|
||||
struct ShardokLatencyTraceConfig {
|
||||
bool enabled = false;
|
||||
std::string gameId;
|
||||
std::chrono::seconds ttl = std::chrono::seconds::zero();
|
||||
double slowSpanThresholdMs = 0.0;
|
||||
};
|
||||
|
||||
void SetShardokLatencyTraceConfig(const ShardokLatencyTraceConfig& config);
|
||||
auto GetShardokLatencyTraceConfig() -> ShardokLatencyTraceConfig;
|
||||
|
||||
auto ShardokLatencyTraceEnabledForGame(std::string_view gameId) -> bool;
|
||||
|
||||
void EmitShardokLatencyTrace(
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "EagleInterfaceGrpcServer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
@@ -29,8 +30,11 @@ namespace shardok {
|
||||
using grpc::Status;
|
||||
using grpc::StatusCode;
|
||||
using net::eagle0::common::GameSetupInfo;
|
||||
using net::eagle0::common::GetShardokLatencyTraceConfig;
|
||||
using net::eagle0::common::ScopedShardokLatencyTrace;
|
||||
using net::eagle0::common::SetShardokLatencyTraceConfig;
|
||||
using net::eagle0::common::ShardokLatencyFields;
|
||||
using net::eagle0::common::ShardokLatencyTraceConfig;
|
||||
using net::eagle0::common::VictoryCondition;
|
||||
using net::eagle0::shardok::api::PlacementCommand;
|
||||
using net::eagle0::shardok::common::EndGameCondition;
|
||||
@@ -452,6 +456,47 @@ auto EagleInterfaceImpl::GetHexMapNames(
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void PopulateLatencyTraceConfigProto(
|
||||
const ShardokLatencyTraceConfig &config,
|
||||
net::eagle0::common::LatencyTraceConfig *proto) {
|
||||
proto->set_enabled(config.enabled);
|
||||
proto->set_game_id(config.gameId);
|
||||
proto->set_ttl_seconds(config.ttl.count());
|
||||
proto->set_slow_span_threshold_ms(config.slowSpanThresholdMs);
|
||||
}
|
||||
|
||||
auto LatencyTraceConfigFromProto(const net::eagle0::common::LatencyTraceConfig &proto)
|
||||
-> ShardokLatencyTraceConfig {
|
||||
return ShardokLatencyTraceConfig{
|
||||
.enabled = proto.enabled(),
|
||||
.gameId = proto.game_id(),
|
||||
.ttl = std::chrono::seconds(proto.ttl_seconds()),
|
||||
.slowSpanThresholdMs = proto.slow_span_threshold_ms()};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto EagleInterfaceImpl::GetLatencyTraceConfig(
|
||||
ServerContext *context,
|
||||
const GetLatencyTraceConfigRequest * /*request*/,
|
||||
GetLatencyTraceConfigResponse *response) -> Status {
|
||||
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
|
||||
PopulateLatencyTraceConfigProto(GetShardokLatencyTraceConfig(), response->mutable_config());
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::SetLatencyTraceConfig(
|
||||
ServerContext *context,
|
||||
const SetLatencyTraceConfigRequest *request,
|
||||
SetLatencyTraceConfigResponse *response) -> Status {
|
||||
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
|
||||
SetShardokLatencyTraceConfig(LatencyTraceConfigFromProto(request->config()));
|
||||
PopulateLatencyTraceConfigProto(GetShardokLatencyTraceConfig(), response->mutable_config());
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::SubscribeToGame(
|
||||
ServerContext *context,
|
||||
const GameSubscriptionRequest *request,
|
||||
|
||||
@@ -34,6 +34,8 @@ using grpc::Status;
|
||||
using net::eagle0::common::GameSetupInfo;
|
||||
using net::eagle0::common::GameStatusResponse;
|
||||
using net::eagle0::common::GameSubscriptionRequest;
|
||||
using net::eagle0::common::GetLatencyTraceConfigRequest;
|
||||
using net::eagle0::common::GetLatencyTraceConfigResponse;
|
||||
using net::eagle0::common::HexMapNamesRequest;
|
||||
using net::eagle0::common::HexMapNamesResponse;
|
||||
using net::eagle0::common::HexMapRequest;
|
||||
@@ -41,6 +43,8 @@ using net::eagle0::common::HexMapResponse;
|
||||
using net::eagle0::common::NewGameRequest;
|
||||
using net::eagle0::common::PlacementCommandsRequest;
|
||||
using net::eagle0::common::PostCommandRequest;
|
||||
using net::eagle0::common::SetLatencyTraceConfigRequest;
|
||||
using net::eagle0::common::SetLatencyTraceConfigResponse;
|
||||
using net::eagle0::common::ShardokInternalInterface;
|
||||
using std::shared_ptr;
|
||||
|
||||
@@ -83,6 +87,14 @@ public:
|
||||
ServerContext* context,
|
||||
const HexMapNamesRequest* request,
|
||||
HexMapNamesResponse* response) -> Status override;
|
||||
auto GetLatencyTraceConfig(
|
||||
ServerContext* context,
|
||||
const GetLatencyTraceConfigRequest* request,
|
||||
GetLatencyTraceConfigResponse* response) -> Status override;
|
||||
auto SetLatencyTraceConfig(
|
||||
ServerContext* context,
|
||||
const SetLatencyTraceConfigRequest* request,
|
||||
SetLatencyTraceConfigResponse* response) -> Status override;
|
||||
|
||||
auto SubscribeToGame(
|
||||
ServerContext* context,
|
||||
|
||||
@@ -15,6 +15,7 @@ go_library(
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//src/main/go/net/eagle0/util/aws",
|
||||
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_go_grpc",
|
||||
"//src/main/protobuf/net/eagle0/eagle/admin:game_admin_go_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_go_proto", # keep
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/admin:admin_go_proto",
|
||||
|
||||
@@ -23,10 +23,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
|
||||
commonpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/common/shardok_internal_interface"
|
||||
gameadminpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/admin"
|
||||
adminpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/api/admin"
|
||||
authpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/api/eagle"
|
||||
gameadminpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/admin"
|
||||
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
@@ -61,35 +62,36 @@ func getBuildInfo() BuildInfo {
|
||||
var eagleContainerName string
|
||||
|
||||
var (
|
||||
eagleAddr = flag.String("eagle-addr", "localhost:40032", "Eagle gRPC server address")
|
||||
authAddr = flag.String("auth-addr", "localhost:40033", "Auth gRPC server address")
|
||||
authTLS = flag.Bool("auth-tls", false, "Use TLS for auth server connection (required for remote auth servers)")
|
||||
jfrSidecarAddr = flag.String("jfr-sidecar-addr", "localhost:8081", "JFR sidecar HTTP address")
|
||||
httpPort = flag.Int("http-port", 8080, "HTTP server port")
|
||||
mapsFlag = flag.String("maps-dir", "", "Directory containing .e0mj map files (enables map editor)")
|
||||
githubRepoFlag = flag.String("github-repo", "nolen777/eagle0", "GitHub repo for map PR creation")
|
||||
mapsRepoPathFlag = flag.String("maps-repo-path", "src/main/resources/net/eagle0/shardok/maps", "Path to maps dir in repo")
|
||||
gameAdminClient gameadminpb.GameAdminClient
|
||||
adminClient adminpb.AdminClient
|
||||
authClient authpb.AuthClient
|
||||
gamesTemplate *template.Template
|
||||
gameDetailTemplate *template.Template
|
||||
historyRowsTemplate *template.Template
|
||||
actionDetailTemplate *template.Template
|
||||
gameStateTemplate *template.Template
|
||||
settingsTemplate *template.Template
|
||||
settingsRowsTemplate *template.Template
|
||||
usersTemplate *template.Template
|
||||
usersRowsTemplate *template.Template
|
||||
invitationsTemplate *template.Template
|
||||
invitationsRowsTemplate *template.Template
|
||||
accountsTemplate *template.Template
|
||||
loginTemplate *template.Template
|
||||
myAccountTemplate *template.Template
|
||||
goodbyeTemplate *template.Template
|
||||
whatsNewTemplate *template.Template
|
||||
llmStatsTemplate *template.Template
|
||||
errorTemplate *template.Template
|
||||
eagleAddr = flag.String("eagle-addr", "localhost:40032", "Eagle gRPC server address")
|
||||
authAddr = flag.String("auth-addr", "localhost:40033", "Auth gRPC server address")
|
||||
authTLS = flag.Bool("auth-tls", false, "Use TLS for auth server connection (required for remote auth servers)")
|
||||
jfrSidecarAddr = flag.String("jfr-sidecar-addr", "localhost:8081", "JFR sidecar HTTP address")
|
||||
httpPort = flag.Int("http-port", 8080, "HTTP server port")
|
||||
mapsFlag = flag.String("maps-dir", "", "Directory containing .e0mj map files (enables map editor)")
|
||||
githubRepoFlag = flag.String("github-repo", "nolen777/eagle0", "GitHub repo for map PR creation")
|
||||
mapsRepoPathFlag = flag.String("maps-repo-path", "src/main/resources/net/eagle0/shardok/maps", "Path to maps dir in repo")
|
||||
gameAdminClient gameadminpb.GameAdminClient
|
||||
adminClient adminpb.AdminClient
|
||||
authClient authpb.AuthClient
|
||||
gamesTemplate *template.Template
|
||||
gameDetailTemplate *template.Template
|
||||
historyRowsTemplate *template.Template
|
||||
actionDetailTemplate *template.Template
|
||||
gameStateTemplate *template.Template
|
||||
settingsTemplate *template.Template
|
||||
settingsRowsTemplate *template.Template
|
||||
usersTemplate *template.Template
|
||||
usersRowsTemplate *template.Template
|
||||
invitationsTemplate *template.Template
|
||||
invitationsRowsTemplate *template.Template
|
||||
accountsTemplate *template.Template
|
||||
loginTemplate *template.Template
|
||||
myAccountTemplate *template.Template
|
||||
goodbyeTemplate *template.Template
|
||||
whatsNewTemplate *template.Template
|
||||
llmStatsTemplate *template.Template
|
||||
latencyTraceTemplate *template.Template
|
||||
errorTemplate *template.Template
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -182,6 +184,11 @@ func main() {
|
||||
log.Fatalf("Failed to parse llm_stats template: %v", err)
|
||||
}
|
||||
|
||||
latencyTraceTemplate, err = template.ParseFS(templatesFS, "templates/layout.html", "templates/latency_trace.html")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse latency_trace template: %v", err)
|
||||
}
|
||||
|
||||
errorTemplate, err = template.ParseFS(templatesFS, "templates/layout.html", "templates/error.html")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse error template: %v", err)
|
||||
@@ -254,6 +261,7 @@ func main() {
|
||||
http.HandleFunc("/settings", requireAuth(handleSettingsPage))
|
||||
http.HandleFunc("/settings/", requireAuth(handleSettingsRoutes))
|
||||
http.HandleFunc("/llm-stats", requireAuth(handleLlmStatsPage))
|
||||
http.HandleFunc("/latency-trace", requireAuth(handleLatencyTracePage))
|
||||
http.HandleFunc("/accounts", requireAuth(handleAccountsPage))
|
||||
http.HandleFunc("/users", requireAuth(handleUsersPage))
|
||||
http.HandleFunc("/users/", requireAuth(handleUserRoutes))
|
||||
@@ -268,6 +276,7 @@ func main() {
|
||||
http.HandleFunc("/logout", handleLogout)
|
||||
http.HandleFunc("/api/games", requireAuth(handleGamesAPI))
|
||||
http.HandleFunc("/api/games/", requireAuth(handleGameHistoryAPI))
|
||||
http.HandleFunc("/api/latency-trace", requireAuth(handleLatencyTraceAPI))
|
||||
http.HandleFunc("/jfr/status", requireAuth(handleJfrStatus))
|
||||
http.HandleFunc("/jfr/start", requireAuth(handleJfrStart))
|
||||
http.HandleFunc("/jfr/stop", requireAuth(handleJfrStop))
|
||||
@@ -1974,6 +1983,139 @@ func handleLlmStatsPage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
type latencyTraceConfigView struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
GameID string `json:"game_id"`
|
||||
TTLSeconds int64 `json:"ttl_seconds"`
|
||||
SlowSpanThresholdMs float64 `json:"slow_span_threshold_ms"`
|
||||
}
|
||||
|
||||
type LatencyTracePageData struct {
|
||||
Title string
|
||||
EagleConfig latencyTraceConfigView
|
||||
ShardokConfig latencyTraceConfigView
|
||||
Error string
|
||||
BuildInfo BuildInfo
|
||||
}
|
||||
|
||||
func latencyTraceConfigFromProto(config *commonpb.LatencyTraceConfig) latencyTraceConfigView {
|
||||
if config == nil {
|
||||
return latencyTraceConfigView{}
|
||||
}
|
||||
return latencyTraceConfigView{
|
||||
Enabled: config.Enabled,
|
||||
GameID: config.GameId,
|
||||
TTLSeconds: config.TtlSeconds,
|
||||
SlowSpanThresholdMs: config.SlowSpanThresholdMs,
|
||||
}
|
||||
}
|
||||
|
||||
func latencyTraceConfigToProto(config latencyTraceConfigView) *commonpb.LatencyTraceConfig {
|
||||
return &commonpb.LatencyTraceConfig{
|
||||
Enabled: config.Enabled,
|
||||
GameId: strings.TrimSpace(config.GameID),
|
||||
TtlSeconds: config.TTLSeconds,
|
||||
SlowSpanThresholdMs: config.SlowSpanThresholdMs,
|
||||
}
|
||||
}
|
||||
|
||||
func handleLatencyTracePage(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := createAdminContext(r)
|
||||
defer cancel()
|
||||
|
||||
resp, err := gameAdminClient.GetShardokLatencyTraceConfig(ctx, &gameadminpb.GetShardokLatencyTraceConfigRequest{})
|
||||
data := LatencyTracePageData{
|
||||
Title: "Shardok Latency Trace",
|
||||
BuildInfo: getBuildInfo(),
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Failed to get latency trace config: %v", err)
|
||||
data.Error = err.Error()
|
||||
} else {
|
||||
data.EagleConfig = latencyTraceConfigFromProto(resp.EagleConfig)
|
||||
data.ShardokConfig = latencyTraceConfigFromProto(resp.ShardokConfig)
|
||||
data.Error = resp.Error
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if err := latencyTraceTemplate.ExecuteTemplate(w, "layout.html", data); err != nil {
|
||||
log.Printf("Template error: %v", err)
|
||||
http.Error(w, "Template error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func handleLatencyTraceAPI(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
ctx, cancel := createAdminContext(r)
|
||||
defer cancel()
|
||||
|
||||
resp, err := gameAdminClient.GetShardokLatencyTraceConfig(ctx, &gameadminpb.GetShardokLatencyTraceConfigRequest{})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
|
||||
case http.MethodPost:
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Invalid form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
enabled := r.FormValue("enabled") == "on" || r.FormValue("enabled") == "true"
|
||||
ttlSeconds, err := strconv.ParseInt(defaultString(r.FormValue("ttl_seconds"), "0"), 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid ttl_seconds", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
slowSpanThresholdMs, err := strconv.ParseFloat(defaultString(r.FormValue("slow_span_threshold_ms"), "0"), 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid slow_span_threshold_ms", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
config := latencyTraceConfigToProto(latencyTraceConfigView{
|
||||
Enabled: enabled,
|
||||
GameID: r.FormValue("game_id"),
|
||||
TTLSeconds: ttlSeconds,
|
||||
SlowSpanThresholdMs: slowSpanThresholdMs,
|
||||
})
|
||||
|
||||
ctx, cancel := createAdminContext(r)
|
||||
defer cancel()
|
||||
|
||||
resp, err := gameAdminClient.SetShardokLatencyTraceConfig(
|
||||
ctx,
|
||||
&gameadminpb.SetShardokLatencyTraceConfigRequest{Config: config},
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !resp.Success {
|
||||
http.Error(w, resp.Error, http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/latency-trace", http.StatusSeeOther)
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultString(value string, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Player management handlers
|
||||
|
||||
func handleAiToHuman(w http.ResponseWriter, r *http.Request, gameIDHex string, gameID int64) {
|
||||
@@ -2565,15 +2707,15 @@ func handleDeleteUser(w http.ResponseWriter, r *http.Request, userID string) {
|
||||
|
||||
// InvitationInfo represents an invitation for display
|
||||
type InvitationInfo struct {
|
||||
InvitationCode string
|
||||
InvitationCodeShort string
|
||||
Email string
|
||||
StatusName string
|
||||
CreatedByUserID string
|
||||
CreatedByDisplayName string
|
||||
CreatedAt string
|
||||
ExpiresAt string
|
||||
RedeemedByUserID string
|
||||
InvitationCode string
|
||||
InvitationCodeShort string
|
||||
Email string
|
||||
StatusName string
|
||||
CreatedByUserID string
|
||||
CreatedByDisplayName string
|
||||
CreatedAt string
|
||||
ExpiresAt string
|
||||
RedeemedByUserID string
|
||||
RedeemedByDisplayName string
|
||||
}
|
||||
|
||||
@@ -3567,12 +3709,12 @@ type WhatsNewData struct {
|
||||
|
||||
// WhatsNewPageData is the data for the what's-new page template
|
||||
type WhatsNewPageData struct {
|
||||
Title string
|
||||
Entries []WhatsNewEntry
|
||||
BuildInfo BuildInfo
|
||||
PreviewMode bool
|
||||
Title string
|
||||
Entries []WhatsNewEntry
|
||||
BuildInfo BuildInfo
|
||||
PreviewMode bool
|
||||
PreviewEntry WhatsNewEntry
|
||||
Error string
|
||||
Error string
|
||||
}
|
||||
|
||||
// fetchWhatsNewData fetches the what's-new JSON directly from S3.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
{{define "content"}}
|
||||
<h1>Shardok Latency Trace</h1>
|
||||
|
||||
{{if .Error}}
|
||||
<article class="error">
|
||||
<strong>Error:</strong> {{.Error}}
|
||||
</article>
|
||||
{{end}}
|
||||
|
||||
<section>
|
||||
<h2>Current Config</h2>
|
||||
<div class="grid">
|
||||
<article>
|
||||
<h3>Eagle</h3>
|
||||
<dl>
|
||||
<dt>Enabled</dt>
|
||||
<dd>{{.EagleConfig.Enabled}}</dd>
|
||||
<dt>Shardok Game ID</dt>
|
||||
<dd>{{if .EagleConfig.GameID}}{{.EagleConfig.GameID}}{{else}}all games{{end}}</dd>
|
||||
<dt>TTL Seconds</dt>
|
||||
<dd>{{.EagleConfig.TTLSeconds}}</dd>
|
||||
<dt>Slow Span Threshold Ms</dt>
|
||||
<dd>{{.EagleConfig.SlowSpanThresholdMs}}</dd>
|
||||
</dl>
|
||||
</article>
|
||||
<article>
|
||||
<h3>Shardok</h3>
|
||||
<dl>
|
||||
<dt>Enabled</dt>
|
||||
<dd>{{.ShardokConfig.Enabled}}</dd>
|
||||
<dt>Shardok Game ID</dt>
|
||||
<dd>{{if .ShardokConfig.GameID}}{{.ShardokConfig.GameID}}{{else}}all games{{end}}</dd>
|
||||
<dt>TTL Seconds</dt>
|
||||
<dd>{{.ShardokConfig.TTLSeconds}}</dd>
|
||||
<dt>Slow Span Threshold Ms</dt>
|
||||
<dd>{{.ShardokConfig.SlowSpanThresholdMs}}</dd>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Update Config</h2>
|
||||
<form method="post" action="/api/latency-trace">
|
||||
<label>
|
||||
<input type="checkbox" name="enabled" {{if .EagleConfig.Enabled}}checked{{end}}>
|
||||
Enable tracing
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Shardok game ID
|
||||
<input type="text" name="game_id" value="{{.EagleConfig.GameID}}" placeholder="blank traces all games">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
TTL seconds
|
||||
<input type="number" name="ttl_seconds" min="0" step="1" value="{{.EagleConfig.TTLSeconds}}">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Slow span threshold ms
|
||||
<input type="number" name="slow_span_threshold_ms" min="0" step="1" value="{{.EagleConfig.SlowSpanThresholdMs}}">
|
||||
</label>
|
||||
|
||||
<button type="submit">Apply</button>
|
||||
</form>
|
||||
<form method="post" action="/api/latency-trace">
|
||||
<input type="hidden" name="enabled" value="false">
|
||||
<button type="submit" class="secondary">Disable</button>
|
||||
</form>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -16,6 +16,7 @@
|
||||
<li><a href="/accounts">Accounts</a></li>
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li><a href="/llm-stats">LLM Stats</a></li>
|
||||
<li><a href="/latency-trace">Latency Trace</a></li>
|
||||
<li><a href="/maps">Maps</a></li>
|
||||
<li><a href="/whats-new">What's New</a></li>
|
||||
<li id="health-status" hx-get="/health/status" hx-trigger="load, every 10s" hx-swap="innerHTML">
|
||||
|
||||
@@ -29,6 +29,30 @@ service ShardokInternalInterface {
|
||||
|
||||
rpc GetHexMap(HexMapRequest) returns (HexMapResponse) {}
|
||||
rpc GetHexMapNames(HexMapNamesRequest) returns (HexMapNamesResponse) {}
|
||||
|
||||
rpc GetLatencyTraceConfig(GetLatencyTraceConfigRequest) returns (GetLatencyTraceConfigResponse) {}
|
||||
rpc SetLatencyTraceConfig(SetLatencyTraceConfigRequest) returns (SetLatencyTraceConfigResponse) {}
|
||||
}
|
||||
|
||||
message LatencyTraceConfig {
|
||||
bool enabled = 1;
|
||||
string game_id = 2;
|
||||
int64 ttl_seconds = 3;
|
||||
double slow_span_threshold_ms = 4;
|
||||
}
|
||||
|
||||
message GetLatencyTraceConfigRequest {}
|
||||
|
||||
message GetLatencyTraceConfigResponse {
|
||||
LatencyTraceConfig config = 1;
|
||||
}
|
||||
|
||||
message SetLatencyTraceConfigRequest {
|
||||
LatencyTraceConfig config = 1;
|
||||
}
|
||||
|
||||
message SetLatencyTraceConfigResponse {
|
||||
LatencyTraceConfig config = 1;
|
||||
}
|
||||
|
||||
message GameSubscriptionRequest {
|
||||
|
||||
@@ -8,6 +8,7 @@ proto_library(
|
||||
srcs = ["game_admin.proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_proto",
|
||||
"@com_google_protobuf//:wrappers_proto",
|
||||
],
|
||||
)
|
||||
@@ -31,4 +32,5 @@ go_proto_library(
|
||||
importpath = "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/admin", # keep
|
||||
proto = ":game_admin_proto", # keep
|
||||
visibility = ["//visibility:public"],
|
||||
deps = ["//src/main/protobuf/net/eagle0/common:shardok_internal_interface_go_grpc"],
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ syntax = "proto3";
|
||||
package net.eagle0.eagle.admin;
|
||||
|
||||
import "google/protobuf/wrappers.proto";
|
||||
import "src/main/protobuf/net/eagle0/common/shardok_internal_interface.proto";
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "net.eagle0.eagle.admin";
|
||||
@@ -72,6 +73,12 @@ service GameAdmin {
|
||||
|
||||
// Rewind a game by truncating persisted chunk files (bypasses game loading)
|
||||
rpc RawRewindGame(RawRewindGameRequest) returns (RawRewindGameResponse) {}
|
||||
|
||||
// Get Eagle and Shardok latency tracing configuration.
|
||||
rpc GetShardokLatencyTraceConfig(GetShardokLatencyTraceConfigRequest) returns (GetShardokLatencyTraceConfigResponse) {}
|
||||
|
||||
// Set Eagle and Shardok latency tracing configuration.
|
||||
rpc SetShardokLatencyTraceConfig(SetShardokLatencyTraceConfigRequest) returns (SetShardokLatencyTraceConfigResponse) {}
|
||||
}
|
||||
|
||||
// GetRunningGames
|
||||
@@ -180,6 +187,25 @@ message AddSettingsKeyValue {
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message GetShardokLatencyTraceConfigRequest {}
|
||||
|
||||
message GetShardokLatencyTraceConfigResponse {
|
||||
.net.eagle0.common.LatencyTraceConfig eagle_config = 1;
|
||||
.net.eagle0.common.LatencyTraceConfig shardok_config = 2;
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
message SetShardokLatencyTraceConfigRequest {
|
||||
.net.eagle0.common.LatencyTraceConfig config = 1;
|
||||
}
|
||||
|
||||
message SetShardokLatencyTraceConfigResponse {
|
||||
.net.eagle0.common.LatencyTraceConfig eagle_config = 1;
|
||||
.net.eagle0.common.LatencyTraceConfig shardok_config = 2;
|
||||
bool success = 3;
|
||||
string error = 4;
|
||||
}
|
||||
|
||||
// ConvertAiToHuman
|
||||
message ConvertAiToHumanRequest {
|
||||
int64 game_id = 1;
|
||||
|
||||
@@ -52,6 +52,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update_receiver",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:shardok_interface_grpc_client",
|
||||
"@maven//:com_thesamet_scalapb_scalapb_json4s_3",
|
||||
"@maven//:io_sentry_sentry",
|
||||
"@maven//:org_json4s_json4s_ast_3",
|
||||
|
||||
@@ -18,6 +18,7 @@ import net.eagle0.common.llm_integration.{
|
||||
LlmUsageTracker,
|
||||
OpenAIChatCompletionsServiceImpl
|
||||
}
|
||||
import net.eagle0.common.shardok_internal_interface.LatencyTraceConfig
|
||||
import net.eagle0.common.SimpleTimedLogger
|
||||
import net.eagle0.eagle.*
|
||||
import net.eagle0.eagle.admin.game_admin.*
|
||||
@@ -27,6 +28,7 @@ import net.eagle0.eagle.library.settings.loaders.SettingsLoader
|
||||
import net.eagle0.eagle.library.EagleClientException
|
||||
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
|
||||
import net.eagle0.eagle.service.persistence.SaveDirectory
|
||||
import net.eagle0.eagle.shardok_interface.ShardokLatencyTrace
|
||||
import net.eagle0.eagle.UserId
|
||||
import org.json4s.*
|
||||
import org.json4s.native.Serialization.writePretty
|
||||
@@ -395,6 +397,56 @@ class GameAdminServiceImpl(
|
||||
end if
|
||||
}
|
||||
|
||||
override def getShardokLatencyTraceConfig(
|
||||
request: GetShardokLatencyTraceConfigRequest
|
||||
): Future[GetShardokLatencyTraceConfigResponse] =
|
||||
gamesManager.shardokClient
|
||||
.getLatencyTraceConfig()
|
||||
.map { shardokConfig =>
|
||||
GetShardokLatencyTraceConfigResponse(
|
||||
eagleConfig = Some(ShardokLatencyTrace.getConfig),
|
||||
shardokConfig = Some(shardokConfig)
|
||||
)
|
||||
}
|
||||
.recover {
|
||||
case e: Exception =>
|
||||
Sentry.captureException(e)
|
||||
GetShardokLatencyTraceConfigResponse(
|
||||
eagleConfig = Some(ShardokLatencyTrace.getConfig),
|
||||
error = e.getMessage
|
||||
)
|
||||
}
|
||||
|
||||
override def setShardokLatencyTraceConfig(
|
||||
request: SetShardokLatencyTraceConfigRequest
|
||||
): Future[SetShardokLatencyTraceConfigResponse] = {
|
||||
val requestedConfig = request.config.getOrElse(LatencyTraceConfig())
|
||||
val eagleConfig = ShardokLatencyTrace.setConfig(requestedConfig)
|
||||
gamesManager.shardokClient
|
||||
.setLatencyTraceConfig(requestedConfig)
|
||||
.map { shardokConfig =>
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Shardok latency tracing updated: enabled=${requestedConfig.enabled}, " +
|
||||
s"gameId=${requestedConfig.gameId}, ttlSeconds=${requestedConfig.ttlSeconds}, " +
|
||||
s"slowSpanThresholdMs=${requestedConfig.slowSpanThresholdMs}"
|
||||
)
|
||||
SetShardokLatencyTraceConfigResponse(
|
||||
eagleConfig = Some(eagleConfig),
|
||||
shardokConfig = Some(shardokConfig),
|
||||
success = true
|
||||
)
|
||||
}
|
||||
.recover {
|
||||
case e: Exception =>
|
||||
Sentry.captureException(e)
|
||||
SetShardokLatencyTraceConfigResponse(
|
||||
eagleConfig = Some(ShardokLatencyTrace.getConfig),
|
||||
success = false,
|
||||
error = e.getMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override def convertAiToHuman(
|
||||
request: ConvertAiToHumanRequest
|
||||
): Future[ConvertAiToHumanResponse] = Future.successful {
|
||||
|
||||
@@ -197,6 +197,7 @@ scala_library(
|
||||
deps = [
|
||||
":placement_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/common:game_setup_info_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_scala_grpc",
|
||||
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:hex_map_scala_proto",
|
||||
|
||||
@@ -484,6 +484,16 @@ class ShardokInterfaceGrpcClient(
|
||||
.withRetries(_.getHexMapNames(HexMapNamesRequest()))
|
||||
.map(_.mapNames.toVector)
|
||||
|
||||
override def getLatencyTraceConfig(): Future[LatencyTraceConfig] =
|
||||
retrier
|
||||
.withRetries(_.getLatencyTraceConfig(GetLatencyTraceConfigRequest()))
|
||||
.map(_.config.getOrElse(LatencyTraceConfig()))
|
||||
|
||||
override def setLatencyTraceConfig(config: LatencyTraceConfig): Future[LatencyTraceConfig] =
|
||||
retrier
|
||||
.withRetries(_.setLatencyTraceConfig(SetLatencyTraceConfigRequest(config = Some(config))))
|
||||
.map(_.config.getOrElse(config))
|
||||
|
||||
override def cancelBattlesForGame(eagleGameId: GameId): Unit =
|
||||
pendingBattles.synchronized {
|
||||
val toRemove = pendingBattles.collect {
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.eagle0.eagle.shardok_interface
|
||||
import scala.concurrent.Future
|
||||
|
||||
import net.eagle0.common.game_setup_info.NewGameRequest
|
||||
import net.eagle0.common.shardok_internal_interface.LatencyTraceConfig
|
||||
import net.eagle0.common.tutorial_battle_config.TutorialBattleConfig
|
||||
import net.eagle0.eagle.{FactionId, GameId, ShardokGameId}
|
||||
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle
|
||||
@@ -44,6 +45,8 @@ trait ShardokInterfaceProxy {
|
||||
|
||||
def getHexMap(mapName: String): Future[HexMap]
|
||||
def getHexMapNames(): Future[Seq[String]]
|
||||
def getLatencyTraceConfig(): Future[LatencyTraceConfig]
|
||||
def setLatencyTraceConfig(config: LatencyTraceConfig): Future[LatencyTraceConfig]
|
||||
|
||||
/** Cancels all pending Shardok battles for the given Eagle game. */
|
||||
def cancelBattlesForGame(eagleGameId: GameId): Unit
|
||||
|
||||
@@ -1,16 +1,63 @@
|
||||
package net.eagle0.eagle.shardok_interface
|
||||
|
||||
import net.eagle0.common.shardok_internal_interface.LatencyTraceConfig
|
||||
|
||||
object ShardokLatencyTrace {
|
||||
private val enabledValues = Set("1", "true", "TRUE", "yes", "YES")
|
||||
|
||||
private lazy val enabled: Boolean =
|
||||
sys.env.get("EAGLE0_SHARDOK_LATENCY_TRACE").exists(enabledValues.contains)
|
||||
private case class ActiveConfig(
|
||||
enabled: Boolean,
|
||||
gameId: String,
|
||||
ttlSeconds: Long,
|
||||
slowSpanThresholdMs: Double,
|
||||
expiresAtNanos: Option[Long]
|
||||
)
|
||||
|
||||
private lazy val gameIdFilter: Option[String] =
|
||||
sys.env.get("EAGLE0_SHARDOK_LATENCY_GAME_ID")
|
||||
private def initialConfig: ActiveConfig =
|
||||
ActiveConfig(
|
||||
enabled = sys.env.get("EAGLE0_SHARDOK_LATENCY_TRACE").exists(enabledValues.contains),
|
||||
gameId = sys.env.get("EAGLE0_SHARDOK_LATENCY_GAME_ID").getOrElse(""),
|
||||
ttlSeconds = 0L,
|
||||
slowSpanThresholdMs = 0.0,
|
||||
expiresAtNanos = None
|
||||
)
|
||||
|
||||
private def enabledForGame(gameId: String): Boolean =
|
||||
enabled && gameIdFilter.forall(filter => filter.isEmpty || filter == gameId)
|
||||
@volatile private var config: ActiveConfig = initialConfig
|
||||
|
||||
private def activeConfig: ActiveConfig = {
|
||||
val current = config
|
||||
current.expiresAtNanos match {
|
||||
case Some(expiresAtNanos) if System.nanoTime() >= expiresAtNanos =>
|
||||
val expired = current.copy(enabled = false, expiresAtNanos = None)
|
||||
config = expired
|
||||
expired
|
||||
case _ => current
|
||||
}
|
||||
}
|
||||
|
||||
def setConfig(newConfig: LatencyTraceConfig): LatencyTraceConfig = {
|
||||
val expiresAtNanos =
|
||||
if newConfig.enabled && newConfig.ttlSeconds > 0 then Some(System.nanoTime() + newConfig.ttlSeconds * 1000000000L)
|
||||
else None
|
||||
config = ActiveConfig(
|
||||
enabled = newConfig.enabled,
|
||||
gameId = newConfig.gameId,
|
||||
ttlSeconds = newConfig.ttlSeconds,
|
||||
slowSpanThresholdMs = newConfig.slowSpanThresholdMs,
|
||||
expiresAtNanos = expiresAtNanos
|
||||
)
|
||||
getConfig
|
||||
}
|
||||
|
||||
def getConfig: LatencyTraceConfig = {
|
||||
val current = activeConfig
|
||||
LatencyTraceConfig(
|
||||
enabled = current.enabled,
|
||||
gameId = current.gameId,
|
||||
ttlSeconds = current.ttlSeconds,
|
||||
slowSpanThresholdMs = current.slowSpanThresholdMs
|
||||
)
|
||||
}
|
||||
|
||||
def startNanos(): Long = System.nanoTime()
|
||||
|
||||
@@ -20,15 +67,18 @@ object ShardokLatencyTrace {
|
||||
startedAtNanos: Long,
|
||||
fields: Map[String, String] = Map.empty
|
||||
): Unit =
|
||||
if enabledForGame(gameId) then
|
||||
val current = activeConfig
|
||||
if current.enabled && (current.gameId.isEmpty || current.gameId == gameId) then
|
||||
val durationMs = (System.nanoTime() - startedAtNanos).toDouble / 1000000.0
|
||||
val base = Map(
|
||||
"event" -> "shardok_latency",
|
||||
"component" -> "eagle",
|
||||
"span" -> span,
|
||||
"game_id" -> gameId
|
||||
)
|
||||
Console.err.println(toJson(base ++ fields, durationMs))
|
||||
if current.slowSpanThresholdMs <= 0.0 || durationMs >= current.slowSpanThresholdMs then
|
||||
val base = Map(
|
||||
"event" -> "shardok_latency",
|
||||
"component" -> "eagle",
|
||||
"span" -> span,
|
||||
"game_id" -> gameId
|
||||
)
|
||||
Console.err.println(toJson(base ++ fields, durationMs))
|
||||
end emit
|
||||
|
||||
def trace[A](
|
||||
span: String,
|
||||
|
||||
Reference in New Issue
Block a user