|
|
|
|
@@ -20,12 +20,537 @@
|
|
|
|
|
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
|
|
|
|
#include "../nostr_core_lib/nostr_core/nip017.h"
|
|
|
|
|
#include "../nostr_core_lib/nostr_core/nip044.h"
|
|
|
|
|
#include "subscriptions.h"
|
|
|
|
|
|
|
|
|
|
// External subscription manager (from main.c via subscriptions.c)
|
|
|
|
|
extern subscription_manager_t g_subscription_manager;
|
|
|
|
|
|
|
|
|
|
// Global variables for config change system
|
|
|
|
|
static pending_config_change_t* pending_changes_head = NULL;
|
|
|
|
|
static int pending_changes_count = 0;
|
|
|
|
|
#define CONFIG_CHANGE_TIMEOUT 300 // 5 minutes
|
|
|
|
|
|
|
|
|
|
// Forward declarations for database functions
|
|
|
|
|
int store_event(cJSON* event);
|
|
|
|
|
int broadcast_event_to_subscriptions(cJSON* event);
|
|
|
|
|
|
|
|
|
|
// Forward declarations for config functions
|
|
|
|
|
char* get_relay_private_key(void);
|
|
|
|
|
const char* get_config_value(const char* key);
|
|
|
|
|
int get_config_bool(const char* key, int default_value);
|
|
|
|
|
int update_config_in_table(const char* key, const char* value);
|
|
|
|
|
|
|
|
|
|
// Monitoring system state
|
|
|
|
|
static time_t last_report_time = 0;
|
|
|
|
|
|
|
|
|
|
// Forward declaration for monitoring helper function
|
|
|
|
|
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void));
|
|
|
|
|
|
|
|
|
|
// Monitoring system helper functions
|
|
|
|
|
int is_monitoring_enabled(void) {
|
|
|
|
|
return get_config_bool("kind_34567_reporting_enabled", 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int get_monitoring_throttle_seconds(void) {
|
|
|
|
|
return get_config_int("kind_34567_reporting_throttling_sec", 5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int set_monitoring_enabled(int enabled) {
|
|
|
|
|
const char* value = enabled ? "1" : "0";
|
|
|
|
|
if (update_config_in_table("kind_34567_reporting_enabled", value) == 0) {
|
|
|
|
|
DEBUG_INFO("Monitoring enabled state changed");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query event kind distribution from database
|
|
|
|
|
cJSON* query_event_kind_distribution(void) {
|
|
|
|
|
extern sqlite3* g_db;
|
|
|
|
|
if (!g_db) {
|
|
|
|
|
DEBUG_ERROR("Database not available for monitoring query");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query event kinds distribution with total count
|
|
|
|
|
sqlite3_stmt* stmt;
|
|
|
|
|
const char* sql = "SELECT kind, COUNT(*) as count FROM events GROUP BY kind ORDER BY count DESC";
|
|
|
|
|
|
|
|
|
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
DEBUG_ERROR("Failed to prepare event kind distribution query");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON* distribution = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddStringToObject(distribution, "data_type", "event_kinds");
|
|
|
|
|
cJSON_AddNumberToObject(distribution, "timestamp", (double)time(NULL));
|
|
|
|
|
|
|
|
|
|
cJSON* kinds_array = cJSON_CreateArray();
|
|
|
|
|
long long total_events = 0;
|
|
|
|
|
|
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
|
|
|
int kind = sqlite3_column_int(stmt, 0);
|
|
|
|
|
long long count = sqlite3_column_int64(stmt, 1);
|
|
|
|
|
total_events += count;
|
|
|
|
|
|
|
|
|
|
cJSON* kind_obj = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddNumberToObject(kind_obj, "kind", kind);
|
|
|
|
|
cJSON_AddNumberToObject(kind_obj, "count", count);
|
|
|
|
|
cJSON_AddItemToArray(kinds_array, kind_obj);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
|
|
|
|
|
cJSON_AddNumberToObject(distribution, "total_events", total_events);
|
|
|
|
|
cJSON_AddItemToObject(distribution, "kinds", kinds_array);
|
|
|
|
|
|
|
|
|
|
return distribution;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query time-based statistics from database
|
|
|
|
|
cJSON* query_time_based_statistics(void) {
|
|
|
|
|
extern sqlite3* g_db;
|
|
|
|
|
if (!g_db) {
|
|
|
|
|
DEBUG_ERROR("Database not available for time stats query");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
time_t now = time(NULL);
|
|
|
|
|
cJSON* time_stats = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddStringToObject(time_stats, "data_type", "time_stats");
|
|
|
|
|
cJSON_AddNumberToObject(time_stats, "timestamp", (double)now);
|
|
|
|
|
|
|
|
|
|
cJSON* periods_array = cJSON_CreateArray();
|
|
|
|
|
|
|
|
|
|
// Define time periods: 24h, 7d, 30d
|
|
|
|
|
struct {
|
|
|
|
|
const char* period;
|
|
|
|
|
time_t seconds;
|
|
|
|
|
const char* description;
|
|
|
|
|
} periods[] = {
|
|
|
|
|
{"last_24h", 86400, "Events in the last 24 hours"},
|
|
|
|
|
{"last_7d", 604800, "Events in the last 7 days"},
|
|
|
|
|
{"last_30d", 2592000, "Events in the last 30 days"},
|
|
|
|
|
{NULL, 0, NULL}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Get total events count
|
|
|
|
|
sqlite3_stmt* total_stmt;
|
|
|
|
|
const char* total_sql = "SELECT COUNT(*) FROM events";
|
|
|
|
|
long long total_events = 0;
|
|
|
|
|
|
|
|
|
|
if (sqlite3_prepare_v2(g_db, total_sql, -1, &total_stmt, NULL) == SQLITE_OK) {
|
|
|
|
|
if (sqlite3_step(total_stmt) == SQLITE_ROW) {
|
|
|
|
|
total_events = sqlite3_column_int64(total_stmt, 0);
|
|
|
|
|
}
|
|
|
|
|
sqlite3_finalize(total_stmt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query each time period
|
|
|
|
|
for (int i = 0; periods[i].period != NULL; i++) {
|
|
|
|
|
sqlite3_stmt* stmt;
|
|
|
|
|
const char* sql = "SELECT COUNT(*) FROM events WHERE created_at >= ?";
|
|
|
|
|
|
|
|
|
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
DEBUG_ERROR("Failed to prepare time stats query");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
time_t cutoff = now - periods[i].seconds;
|
|
|
|
|
sqlite3_bind_int64(stmt, 1, cutoff);
|
|
|
|
|
|
|
|
|
|
long long count = 0;
|
|
|
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
|
|
|
count = sqlite3_column_int64(stmt, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
|
|
|
|
|
cJSON* period_obj = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddStringToObject(period_obj, "period", periods[i].period);
|
|
|
|
|
cJSON_AddNumberToObject(period_obj, "count", count);
|
|
|
|
|
cJSON_AddStringToObject(period_obj, "description", periods[i].description);
|
|
|
|
|
cJSON_AddItemToArray(periods_array, period_obj);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON_AddItemToObject(time_stats, "periods", periods_array);
|
|
|
|
|
cJSON_AddNumberToObject(time_stats, "total_events", total_events);
|
|
|
|
|
|
|
|
|
|
return time_stats;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query top pubkeys by event count from database
|
|
|
|
|
cJSON* query_top_pubkeys(void) {
|
|
|
|
|
extern sqlite3* g_db;
|
|
|
|
|
if (!g_db) {
|
|
|
|
|
DEBUG_ERROR("Database not available for top pubkeys query");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query top 10 pubkeys by event count
|
|
|
|
|
sqlite3_stmt* stmt;
|
|
|
|
|
const char* sql = "SELECT pubkey, COUNT(*) as count FROM events GROUP BY pubkey ORDER BY count DESC LIMIT 10";
|
|
|
|
|
|
|
|
|
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
DEBUG_ERROR("Failed to prepare top pubkeys query");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON* top_pubkeys = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddStringToObject(top_pubkeys, "data_type", "top_pubkeys");
|
|
|
|
|
cJSON_AddNumberToObject(top_pubkeys, "timestamp", (double)time(NULL));
|
|
|
|
|
|
|
|
|
|
cJSON* pubkeys_array = cJSON_CreateArray();
|
|
|
|
|
|
|
|
|
|
// Get total events count for percentage calculation
|
|
|
|
|
sqlite3_stmt* total_stmt;
|
|
|
|
|
const char* total_sql = "SELECT COUNT(*) FROM events";
|
|
|
|
|
long long total_events = 0;
|
|
|
|
|
|
|
|
|
|
if (sqlite3_prepare_v2(g_db, total_sql, -1, &total_stmt, NULL) == SQLITE_OK) {
|
|
|
|
|
if (sqlite3_step(total_stmt) == SQLITE_ROW) {
|
|
|
|
|
total_events = sqlite3_column_int64(total_stmt, 0);
|
|
|
|
|
}
|
|
|
|
|
sqlite3_finalize(total_stmt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
|
|
|
const char* pubkey = (const char*)sqlite3_column_text(stmt, 0);
|
|
|
|
|
long long count = sqlite3_column_int64(stmt, 1);
|
|
|
|
|
|
|
|
|
|
cJSON* pubkey_obj = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : "");
|
|
|
|
|
cJSON_AddNumberToObject(pubkey_obj, "event_count", count);
|
|
|
|
|
// Percentage will be calculated by frontend using total_events
|
|
|
|
|
cJSON_AddItemToArray(pubkeys_array, pubkey_obj);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
|
|
|
|
|
cJSON_AddItemToObject(top_pubkeys, "pubkeys", pubkeys_array);
|
|
|
|
|
cJSON_AddNumberToObject(top_pubkeys, "total_events", total_events);
|
|
|
|
|
|
|
|
|
|
return top_pubkeys;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query active subscriptions from in-memory manager (NO DATABASE QUERY)
|
|
|
|
|
cJSON* query_active_subscriptions(void) {
|
|
|
|
|
// Access the global subscription manager
|
|
|
|
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
|
|
|
|
|
int total_subs = g_subscription_manager.total_subscriptions;
|
|
|
|
|
int max_subs = g_subscription_manager.max_total_subscriptions;
|
|
|
|
|
int max_per_client = g_subscription_manager.max_subscriptions_per_client;
|
|
|
|
|
|
|
|
|
|
// Calculate per-client statistics by iterating through active subscriptions
|
|
|
|
|
int client_count = 0;
|
|
|
|
|
int most_subs_per_client = 0;
|
|
|
|
|
|
|
|
|
|
// Count subscriptions per WebSocket connection
|
|
|
|
|
subscription_t* current = g_subscription_manager.active_subscriptions;
|
|
|
|
|
struct lws* last_wsi = NULL;
|
|
|
|
|
int current_client_subs = 0;
|
|
|
|
|
|
|
|
|
|
while (current) {
|
|
|
|
|
if (current->wsi != last_wsi) {
|
|
|
|
|
// New client
|
|
|
|
|
if (last_wsi != NULL) {
|
|
|
|
|
client_count++;
|
|
|
|
|
if (current_client_subs > most_subs_per_client) {
|
|
|
|
|
most_subs_per_client = current_client_subs;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
last_wsi = current->wsi;
|
|
|
|
|
current_client_subs = 1;
|
|
|
|
|
} else {
|
|
|
|
|
current_client_subs++;
|
|
|
|
|
}
|
|
|
|
|
current = current->next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle last client
|
|
|
|
|
if (last_wsi != NULL) {
|
|
|
|
|
client_count++;
|
|
|
|
|
if (current_client_subs > most_subs_per_client) {
|
|
|
|
|
most_subs_per_client = current_client_subs;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
|
|
|
|
|
// Calculate statistics
|
|
|
|
|
double utilization_percentage = max_subs > 0 ? (total_subs * 100.0 / max_subs) : 0.0;
|
|
|
|
|
double avg_subs_per_client = client_count > 0 ? (total_subs * 1.0 / client_count) : 0.0;
|
|
|
|
|
|
|
|
|
|
// Build JSON response matching the design spec
|
|
|
|
|
cJSON* subscriptions = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddStringToObject(subscriptions, "data_type", "active_subscriptions");
|
|
|
|
|
cJSON_AddNumberToObject(subscriptions, "timestamp", (double)time(NULL));
|
|
|
|
|
|
|
|
|
|
cJSON* data = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddNumberToObject(data, "total_subscriptions", total_subs);
|
|
|
|
|
cJSON_AddNumberToObject(data, "max_subscriptions", max_subs);
|
|
|
|
|
cJSON_AddNumberToObject(data, "utilization_percentage", utilization_percentage);
|
|
|
|
|
cJSON_AddNumberToObject(data, "subscriptions_per_client_avg", avg_subs_per_client);
|
|
|
|
|
cJSON_AddNumberToObject(data, "most_subscriptions_per_client", most_subs_per_client);
|
|
|
|
|
cJSON_AddNumberToObject(data, "max_subscriptions_per_client", max_per_client);
|
|
|
|
|
cJSON_AddNumberToObject(data, "active_clients", client_count);
|
|
|
|
|
|
|
|
|
|
cJSON_AddItemToObject(subscriptions, "data", data);
|
|
|
|
|
|
|
|
|
|
return subscriptions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query detailed subscription information from in-memory manager (ADMIN ONLY)
|
|
|
|
|
cJSON* query_subscription_details(void) {
|
|
|
|
|
// Access the global subscription manager
|
|
|
|
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
|
|
|
|
|
time_t current_time = time(NULL);
|
|
|
|
|
cJSON* subscriptions_data = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddStringToObject(subscriptions_data, "data_type", "subscription_details");
|
|
|
|
|
cJSON_AddNumberToObject(subscriptions_data, "timestamp", (double)current_time);
|
|
|
|
|
|
|
|
|
|
cJSON* data = cJSON_CreateObject();
|
|
|
|
|
cJSON* subscriptions_array = cJSON_CreateArray();
|
|
|
|
|
|
|
|
|
|
// Iterate through all active subscriptions
|
|
|
|
|
subscription_t* current = g_subscription_manager.active_subscriptions;
|
|
|
|
|
while (current) {
|
|
|
|
|
cJSON* sub_obj = cJSON_CreateObject();
|
|
|
|
|
|
|
|
|
|
// Basic subscription info
|
|
|
|
|
cJSON_AddStringToObject(sub_obj, "id", current->id);
|
|
|
|
|
cJSON_AddStringToObject(sub_obj, "client_ip", current->client_ip);
|
|
|
|
|
cJSON_AddNumberToObject(sub_obj, "created_at", (double)current->created_at);
|
|
|
|
|
cJSON_AddNumberToObject(sub_obj, "duration_seconds", (double)(current_time - current->created_at));
|
|
|
|
|
cJSON_AddNumberToObject(sub_obj, "events_sent", current->events_sent);
|
|
|
|
|
cJSON_AddBoolToObject(sub_obj, "active", current->active);
|
|
|
|
|
|
|
|
|
|
// Extract filter details
|
|
|
|
|
cJSON* filters_array = cJSON_CreateArray();
|
|
|
|
|
subscription_filter_t* filter = current->filters;
|
|
|
|
|
|
|
|
|
|
while (filter) {
|
|
|
|
|
cJSON* filter_obj = cJSON_CreateObject();
|
|
|
|
|
|
|
|
|
|
// Add kinds array if present
|
|
|
|
|
if (filter->kinds) {
|
|
|
|
|
cJSON_AddItemToObject(filter_obj, "kinds", cJSON_Duplicate(filter->kinds, 1));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add authors array if present
|
|
|
|
|
if (filter->authors) {
|
|
|
|
|
cJSON_AddItemToObject(filter_obj, "authors", cJSON_Duplicate(filter->authors, 1));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add ids array if present
|
|
|
|
|
if (filter->ids) {
|
|
|
|
|
cJSON_AddItemToObject(filter_obj, "ids", cJSON_Duplicate(filter->ids, 1));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add since/until timestamps if set
|
|
|
|
|
if (filter->since > 0) {
|
|
|
|
|
cJSON_AddNumberToObject(filter_obj, "since", (double)filter->since);
|
|
|
|
|
}
|
|
|
|
|
if (filter->until > 0) {
|
|
|
|
|
cJSON_AddNumberToObject(filter_obj, "until", (double)filter->until);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add limit if set
|
|
|
|
|
if (filter->limit > 0) {
|
|
|
|
|
cJSON_AddNumberToObject(filter_obj, "limit", filter->limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add tag filters if present
|
|
|
|
|
if (filter->tag_filters) {
|
|
|
|
|
cJSON_AddItemToObject(filter_obj, "tag_filters", cJSON_Duplicate(filter->tag_filters, 1));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON_AddItemToArray(filters_array, filter_obj);
|
|
|
|
|
filter = filter->next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON_AddItemToObject(sub_obj, "filters", filters_array);
|
|
|
|
|
cJSON_AddItemToArray(subscriptions_array, sub_obj);
|
|
|
|
|
|
|
|
|
|
current = current->next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
|
|
|
|
|
// Add subscriptions array and count to data
|
|
|
|
|
cJSON_AddItemToObject(data, "subscriptions", subscriptions_array);
|
|
|
|
|
cJSON_AddNumberToObject(data, "total_count", cJSON_GetArraySize(subscriptions_array));
|
|
|
|
|
|
|
|
|
|
cJSON_AddItemToObject(subscriptions_data, "data", data);
|
|
|
|
|
|
|
|
|
|
return subscriptions_data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate and broadcast monitoring event
|
|
|
|
|
int generate_monitoring_event(void) {
|
|
|
|
|
// Generate event_kinds monitoring event
|
|
|
|
|
if (generate_monitoring_event_for_type("event_kinds", query_event_kind_distribution) != 0) {
|
|
|
|
|
DEBUG_ERROR("Failed to generate event_kinds monitoring event");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate time_stats monitoring event
|
|
|
|
|
if (generate_monitoring_event_for_type("time_stats", query_time_based_statistics) != 0) {
|
|
|
|
|
DEBUG_ERROR("Failed to generate time_stats monitoring event");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate top_pubkeys monitoring event
|
|
|
|
|
if (generate_monitoring_event_for_type("top_pubkeys", query_top_pubkeys) != 0) {
|
|
|
|
|
DEBUG_ERROR("Failed to generate top_pubkeys monitoring event");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate active_subscriptions monitoring event
|
|
|
|
|
if (generate_monitoring_event_for_type("active_subscriptions", query_active_subscriptions) != 0) {
|
|
|
|
|
DEBUG_ERROR("Failed to generate active_subscriptions monitoring event");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate subscription_details monitoring event (admin-only)
|
|
|
|
|
if (generate_monitoring_event_for_type("subscription_details", query_subscription_details) != 0) {
|
|
|
|
|
DEBUG_ERROR("Failed to generate subscription_details monitoring event");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DEBUG_INFO("Generated and broadcast all monitoring events");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to generate monitoring event for a specific type
|
|
|
|
|
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void)) {
|
|
|
|
|
// Query the monitoring data
|
|
|
|
|
cJSON* monitoring_data = query_func();
|
|
|
|
|
if (!monitoring_data) {
|
|
|
|
|
DEBUG_ERROR("Failed to query monitoring data for %s", d_tag_value);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert to JSON string for content
|
|
|
|
|
char* content_json = cJSON_Print(monitoring_data);
|
|
|
|
|
cJSON_Delete(monitoring_data);
|
|
|
|
|
|
|
|
|
|
if (!content_json) {
|
|
|
|
|
DEBUG_ERROR("Failed to serialize monitoring data for %s", d_tag_value);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get relay keys for signing
|
|
|
|
|
const char* relay_pubkey = get_config_value("relay_pubkey");
|
|
|
|
|
char* relay_privkey_hex = get_relay_private_key();
|
|
|
|
|
if (!relay_pubkey || !relay_privkey_hex) {
|
|
|
|
|
free(content_json);
|
|
|
|
|
DEBUG_ERROR("Could not get relay keys for monitoring event (%s)", d_tag_value);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert relay private key to bytes
|
|
|
|
|
unsigned char relay_privkey[32];
|
|
|
|
|
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
|
|
|
|
free(relay_privkey_hex);
|
|
|
|
|
free(content_json);
|
|
|
|
|
DEBUG_ERROR("Failed to convert relay private key for monitoring event (%s)", d_tag_value);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
free(relay_privkey_hex);
|
|
|
|
|
|
|
|
|
|
// Create monitoring event (kind 34567)
|
|
|
|
|
cJSON* monitoring_event = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddStringToObject(monitoring_event, "id", ""); // Will be set by signing
|
|
|
|
|
cJSON_AddStringToObject(monitoring_event, "pubkey", relay_pubkey);
|
|
|
|
|
cJSON_AddNumberToObject(monitoring_event, "created_at", (double)time(NULL));
|
|
|
|
|
cJSON_AddNumberToObject(monitoring_event, "kind", 34567);
|
|
|
|
|
cJSON_AddStringToObject(monitoring_event, "content", content_json);
|
|
|
|
|
|
|
|
|
|
// Create tags array with d tag for identification
|
|
|
|
|
cJSON* tags = cJSON_CreateArray();
|
|
|
|
|
|
|
|
|
|
// d tag for event identification
|
|
|
|
|
cJSON* d_tag = cJSON_CreateArray();
|
|
|
|
|
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
|
|
|
|
cJSON_AddItemToArray(d_tag, cJSON_CreateString(d_tag_value));
|
|
|
|
|
cJSON_AddItemToArray(tags, d_tag);
|
|
|
|
|
|
|
|
|
|
cJSON_AddItemToObject(monitoring_event, "tags", tags);
|
|
|
|
|
|
|
|
|
|
// Use the library function to create and sign the event
|
|
|
|
|
cJSON* signed_event = nostr_create_and_sign_event(
|
|
|
|
|
34567, // kind
|
|
|
|
|
cJSON_GetStringValue(cJSON_GetObjectItem(monitoring_event, "content")), // content
|
|
|
|
|
tags, // tags
|
|
|
|
|
relay_privkey, // private key
|
|
|
|
|
(time_t)cJSON_GetNumberValue(cJSON_GetObjectItem(monitoring_event, "created_at")) // timestamp
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!signed_event) {
|
|
|
|
|
cJSON_Delete(monitoring_event);
|
|
|
|
|
free(content_json);
|
|
|
|
|
DEBUG_ERROR("Failed to create and sign monitoring event (%s)", d_tag_value);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Replace the unsigned event with the signed one
|
|
|
|
|
cJSON_Delete(monitoring_event);
|
|
|
|
|
monitoring_event = signed_event;
|
|
|
|
|
|
|
|
|
|
// Broadcast the event to active subscriptions
|
|
|
|
|
broadcast_event_to_subscriptions(monitoring_event);
|
|
|
|
|
|
|
|
|
|
// Store in database
|
|
|
|
|
int store_result = store_event(monitoring_event);
|
|
|
|
|
|
|
|
|
|
cJSON_Delete(monitoring_event);
|
|
|
|
|
free(content_json);
|
|
|
|
|
|
|
|
|
|
if (store_result != 0) {
|
|
|
|
|
DEBUG_ERROR("Failed to store monitoring event (%s)", d_tag_value);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Monitoring hook called when an event is stored
|
|
|
|
|
void monitoring_on_event_stored(void) {
|
|
|
|
|
// Check if monitoring is enabled
|
|
|
|
|
if (!is_monitoring_enabled()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check throttling
|
|
|
|
|
time_t now = time(NULL);
|
|
|
|
|
int throttle_seconds = get_monitoring_throttle_seconds();
|
|
|
|
|
|
|
|
|
|
if (now - last_report_time < throttle_seconds) {
|
|
|
|
|
return; // Too soon, skip this report
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate and broadcast monitoring event
|
|
|
|
|
if (generate_monitoring_event() == 0) {
|
|
|
|
|
last_report_time = now;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize monitoring system
|
|
|
|
|
int init_monitoring_system(void) {
|
|
|
|
|
last_report_time = 0;
|
|
|
|
|
DEBUG_INFO("Monitoring system initialized");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cleanup monitoring system
|
|
|
|
|
void cleanup_monitoring_system(void) {
|
|
|
|
|
// No cleanup needed for monitoring system
|
|
|
|
|
DEBUG_INFO("Monitoring system cleaned up");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Forward declaration for known_configs (defined in config.c)
|
|
|
|
|
typedef struct {
|
|
|
|
|
const char* key;
|
|
|
|
|
@@ -107,9 +632,10 @@ int broadcast_event_to_subscriptions(cJSON* event);
|
|
|
|
|
char* get_relay_private_key(void);
|
|
|
|
|
const char* get_config_value(const char* key);
|
|
|
|
|
int get_config_bool(const char* key, int default_value);
|
|
|
|
|
int update_config_in_table(const char* key, const char* value);
|
|
|
|
|
|
|
|
|
|
// Forward declarations for database functions
|
|
|
|
|
int store_event(cJSON* event);
|
|
|
|
|
// Forward declaration for monitoring helper function
|
|
|
|
|
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void));
|
|
|
|
|
|
|
|
|
|
// Handle HTTP request for embedded files (assumes GET)
|
|
|
|
|
int handle_embedded_file_request(struct lws* wsi, const char* requested_uri) {
|
|
|
|
|
@@ -636,6 +1162,12 @@ char* generate_stats_json(void) {
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddNumberToObject(response, "database_size_bytes", db_size);
|
|
|
|
|
|
|
|
|
|
// Get active subscriptions count from in-memory manager
|
|
|
|
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
int active_subs = g_subscription_manager.total_subscriptions;
|
|
|
|
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
cJSON_AddNumberToObject(response, "active_subscriptions", active_subs);
|
|
|
|
|
|
|
|
|
|
// Query total events count
|
|
|
|
|
sqlite3_stmt* stmt;
|
|
|
|
|
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
|
|
|
|
|
@@ -914,6 +1446,11 @@ char* generate_stats_text(void) {
|
|
|
|
|
long long db_bytes = db_size ? (long long)cJSON_GetNumberValue(db_size) : 0;
|
|
|
|
|
double db_mb = db_bytes / (1024.0 * 1024.0);
|
|
|
|
|
|
|
|
|
|
// Get active subscriptions count from in-memory manager
|
|
|
|
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
int active_subs = g_subscription_manager.total_subscriptions;
|
|
|
|
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
|
|
|
|
|
// Format timestamps
|
|
|
|
|
char oldest_str[64] = "-";
|
|
|
|
|
char newest_str[64] = "-";
|
|
|
|
|
@@ -953,10 +1490,11 @@ char* generate_stats_text(void) {
|
|
|
|
|
"Metric\tValue\tDescription\n"
|
|
|
|
|
"Database Size\t%.2f MB (%lld bytes)\tCurrent database file size\n"
|
|
|
|
|
"Total Events\t%lld\tTotal number of events stored\n"
|
|
|
|
|
"Active Subscriptions\t%d\tCurrent active WebSocket subscriptions\n"
|
|
|
|
|
"Oldest Event\t%s\tTimestamp of oldest event\n"
|
|
|
|
|
"Newest Event\t%s\tTimestamp of newest event\n"
|
|
|
|
|
"\n",
|
|
|
|
|
db_mb, db_bytes, total, oldest_str, newest_str);
|
|
|
|
|
db_mb, db_bytes, total, active_subs, oldest_str, newest_str);
|
|
|
|
|
|
|
|
|
|
// Event Kind Distribution section
|
|
|
|
|
offset += snprintf(stats_text + offset, 16384 - offset,
|
|
|
|
|
@@ -1682,3 +2220,124 @@ int process_config_change_request(const char* admin_pubkey, const char* message)
|
|
|
|
|
free(change_id);
|
|
|
|
|
return 1; // Confirmation sent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle monitoring system admin commands
|
|
|
|
|
int handle_monitoring_command(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi) {
|
|
|
|
|
if (!event || !command || !error_message) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get request event ID for response correlation
|
|
|
|
|
cJSON* request_id_obj = cJSON_GetObjectItem(event, "id");
|
|
|
|
|
if (!request_id_obj || !cJSON_IsString(request_id_obj)) {
|
|
|
|
|
snprintf(error_message, error_size, "Missing request event ID");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
const char* request_id = cJSON_GetStringValue(request_id_obj);
|
|
|
|
|
|
|
|
|
|
// Get sender pubkey for response
|
|
|
|
|
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
|
|
|
|
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
|
|
|
|
|
snprintf(error_message, error_size, "Missing sender pubkey");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
|
|
|
|
|
|
|
|
|
|
// Parse command
|
|
|
|
|
char cmd[256];
|
|
|
|
|
char arg[256];
|
|
|
|
|
cmd[0] = '\0';
|
|
|
|
|
arg[0] = '\0';
|
|
|
|
|
|
|
|
|
|
// Simple command parsing - split on space
|
|
|
|
|
const char* space_pos = strchr(command, ' ');
|
|
|
|
|
if (space_pos) {
|
|
|
|
|
size_t cmd_len = space_pos - command;
|
|
|
|
|
if (cmd_len < sizeof(cmd)) {
|
|
|
|
|
memcpy(cmd, command, cmd_len);
|
|
|
|
|
cmd[cmd_len] = '\0';
|
|
|
|
|
strcpy(arg, space_pos + 1);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
strcpy(cmd, command);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert to lowercase for case-insensitive matching
|
|
|
|
|
for (char* p = cmd; *p; p++) {
|
|
|
|
|
if (*p >= 'A' && *p <= 'Z') *p = *p + 32;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle commands
|
|
|
|
|
if (strcmp(cmd, "enable_monitoring") == 0) {
|
|
|
|
|
if (set_monitoring_enabled(1) == 0) {
|
|
|
|
|
char* response_content = "✅ Monitoring enabled\n\nReal-time monitoring events will now be generated.";
|
|
|
|
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
|
|
|
} else {
|
|
|
|
|
char* response_content = "❌ Failed to enable monitoring";
|
|
|
|
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
|
|
|
}
|
|
|
|
|
} else if (strcmp(cmd, "disable_monitoring") == 0) {
|
|
|
|
|
if (set_monitoring_enabled(0) == 0) {
|
|
|
|
|
char* response_content = "✅ Monitoring disabled\n\nReal-time monitoring events will no longer be generated.";
|
|
|
|
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
|
|
|
} else {
|
|
|
|
|
char* response_content = "❌ Failed to disable monitoring";
|
|
|
|
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
|
|
|
}
|
|
|
|
|
} else if (strcmp(cmd, "set_monitoring_throttle") == 0) {
|
|
|
|
|
if (arg[0] == '\0') {
|
|
|
|
|
char* response_content = "❌ Missing throttle value\n\nUsage: set_monitoring_throttle <seconds>";
|
|
|
|
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char* endptr;
|
|
|
|
|
long throttle_seconds = strtol(arg, &endptr, 10);
|
|
|
|
|
if (*endptr != '\0' || throttle_seconds < 1 || throttle_seconds > 3600) {
|
|
|
|
|
char* response_content = "❌ Invalid throttle value\n\nThrottle must be between 1 and 3600 seconds.";
|
|
|
|
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char throttle_str[16];
|
|
|
|
|
snprintf(throttle_str, sizeof(throttle_str), "%ld", throttle_seconds);
|
|
|
|
|
|
|
|
|
|
if (update_config_in_table("kind_34567_reporting_throttling_sec", throttle_str) == 0) {
|
|
|
|
|
char response_content[256];
|
|
|
|
|
snprintf(response_content, sizeof(response_content),
|
|
|
|
|
"✅ Monitoring throttle updated\n\nMinimum interval between monitoring events: %ld seconds", throttle_seconds);
|
|
|
|
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
|
|
|
} else {
|
|
|
|
|
char* response_content = "❌ Failed to update monitoring throttle";
|
|
|
|
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
|
|
|
}
|
|
|
|
|
} else if (strcmp(cmd, "monitoring_status") == 0) {
|
|
|
|
|
int enabled = is_monitoring_enabled();
|
|
|
|
|
int throttle = get_monitoring_throttle_seconds();
|
|
|
|
|
|
|
|
|
|
char response_content[512];
|
|
|
|
|
snprintf(response_content, sizeof(response_content),
|
|
|
|
|
"📊 Monitoring Status\n"
|
|
|
|
|
"━━━━━━━━━━━━━━━━━━━━\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"Enabled: %s\n"
|
|
|
|
|
"Throttle: %d seconds\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"Commands:\n"
|
|
|
|
|
"• enable_monitoring\n"
|
|
|
|
|
"• disable_monitoring\n"
|
|
|
|
|
"• set_monitoring_throttle <seconds>\n"
|
|
|
|
|
"• monitoring_status",
|
|
|
|
|
enabled ? "Yes" : "No", throttle);
|
|
|
|
|
|
|
|
|
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
|
|
|
} else {
|
|
|
|
|
char response_content[256];
|
|
|
|
|
snprintf(response_content, sizeof(response_content),
|
|
|
|
|
"❌ Unknown monitoring command: %s\n\n"
|
|
|
|
|
"Available commands:\n"
|
|
|
|
|
"• enable_monitoring\n"
|
|
|
|
|
"• disable_monitoring\n"
|
|
|
|
|
"• set_monitoring_throttle <seconds>\n"
|
|
|
|
|
"• monitoring_status", cmd);
|
|
|
|
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|