diff --git a/docs/_docs/hardware/01-genodebs.md b/docs/_docs/hardware/01-genodebs.md index 59a3cc831..736270982 100644 --- a/docs/_docs/hardware/01-genodebs.md +++ b/docs/_docs/hardware/01-genodebs.md @@ -19,13 +19,15 @@ If you have tested radio hardware from a vendor not listed with Open5GS, please * CableFree Small Cell Indoor radios (5G n77, n78 and other bands) * CableFree Macro (BBU+RRH) radios (4G and 5G, various bands) * Ericsson Baseband 6630 (21.Q3 Software) + Radio 2217, Radio 2219 (4G and 5G, various bands) + * Ericsson Baseband 6648/6651 IRU 8848 Dots 4475 (N78, N1,N3) * Ericsson StreetMacro 6701 (21.Q3 Software) (5G mmWave, n261) (Baseband 6318 and AIR 1281 packaged together) - * Huawei BTS5900 + * Huawei BTS5900 V100R019C10SPC220 (N78, N28) * LIONS RANathon O-CU and O-DU + RANathon RS8601 Indoor O-RU + RANathon XG8600 Fronthaul Gateway * NOKIA AEQE (SW: 5G20A) * NOKIA AEQD (SW: 5G20A) * NOKIA AEQP (SW: 5G21A) - * MOSO Networks Canopy 5GID1 Indoor 2T2R (5G n48 n78) + * MOSO Networks Canopy 5GID1 Indoor 2T2R (5G n48 n78) + * ZTE ITBBU ITRAN-PNF V5.65.20.20F10 (n78, n1, n3) ### Commercial 4G --- @@ -44,7 +46,7 @@ If you have tested radio hardware from a vendor not listed with Open5GS, please * Baicells Nova 227 (EBS & CBRS) * Baicells Nova 233 * Baicells Nova 430i (band 48/CBRS, SW version BaiBLQ_3.0.12) - * Ericsson Baseband 6630 (21Q1 Software) + * Ericsson Baseband 6630/6648/6651 (21Q1 Software) * Ericsson RBS 6402 (18.Q1 software, B2 B25 B4 B7 B252 B255) * Ericsson RBS 6601 + DUL 20 01 + RUS 01 B8 * Gemtek WLTGFC-101 (S/W version 2.1.1746.1116) @@ -62,6 +64,7 @@ If you have tested radio hardware from a vendor not listed with Open5GS, please * Mikrotik Intercell B1+B3 IC322GC-b1D+b3D * Ruckus Q710 and Q910 * Sercomm SCE4255W "Englewood" (band 48/CBRS, SW version DG3934v3@2308041842) + * ZTE ITBBU ITRAN-PNF V5.65.20.20F10 ### 4G/5G Software Stacks + SDRs --- @@ -69,9 +72,11 @@ If you have tested radio hardware from a vendor not listed with Open5GS, please * [Amarisoft](https://www.amarisoft.com/) + LimeSDR, USRP, Amarisoft PCI Express Card * Open Air Interface 5G ([NR_SA_F1AP_5GRECORDS branch](https://gitlab.eurecom.fr/oai/openairinterface5g/-/tree/NR_SA_F1AP_5GRECORDS)) + USRP B210 * [srsLTE / srsENB](https://github.com/srsLTE/srsLTE) + LimeSDR, USRP, BladeRF x40 (BladeRF Not stable) + * [srsRAN_Project](https://github.com/srsran/srsRAN_Project) 5G O-RAN CU/DU based on USRP SDR. ### Misc Radio Hardware --- * [OpenAirInterface v1.0.3](https://gitlab.eurecom.fr/oai/openairinterface5g/-/tree/v1.0.3) 4G RAN Simulator * [OsmoBTS](https://osmocom.org/projects/osmobts/wiki) controlled ip.access NanoBTS (Used for CSFB with Osmocom) * [UERANSIM](https://github.com/aligungr/UERANSIM) 5G RAN Simulator + * [PacketRusher](https://github.com/HewlettPackard/PacketRusher) 5G performance testing and validation tool diff --git a/lib/metrics/context.c b/lib/metrics/context.c index 5f5d14e01..8b6327cc2 100644 --- a/lib/metrics/context.c +++ b/lib/metrics/context.c @@ -1,6 +1,7 @@ /* * Copyright (C) 2022 by sysmocom - s.f.m.c. GmbH * Copyright (C) 2023 by Sukchan Lee + * Copyright (C) 2025 by Juraj Elias * * This file is part of Open5GS. * @@ -19,9 +20,31 @@ */ #include "ogs-metrics.h" +#include "ogs-core.h" +#include "metrics/ogs-metrics.h" #define DEFAULT_PROMETHEUS_HTTP_PORT 9090 +/* Global (optional) dumper. NULL when no NF registered. */ +size_t (*ogs_metrics_connected_ues_dumper)(char *buf, size_t buflen) = NULL; +size_t (*ogs_metrics_connected_gnbs_dumper)(char *buf, size_t buflen) = NULL; +size_t (*ogs_metrics_connected_enbs_dumper)(char *buf, size_t buflen) = NULL; + +void ogs_metrics_register_connected_ues(size_t (*fn)(char *buf, size_t buflen)) +{ + ogs_metrics_connected_ues_dumper = fn; +} + +void ogs_metrics_register_connected_gnbs(size_t (*fn)(char *buf, size_t buflen)) +{ + ogs_metrics_connected_gnbs_dumper = fn; +} + +void ogs_metrics_register_connected_enbs(size_t (*fn)(char *buf, size_t buflen)) +{ + ogs_metrics_connected_enbs_dumper = fn; +} + int __ogs_metrics_domain; static ogs_metrics_context_t self; static int context_initialized = 0; diff --git a/lib/metrics/ogs-metrics.h b/lib/metrics/ogs-metrics.h index 7d1117d3c..fc04c97ba 100644 --- a/lib/metrics/ogs-metrics.h +++ b/lib/metrics/ogs-metrics.h @@ -20,25 +20,44 @@ #ifndef OGS_METRICS_H #define OGS_METRICS_H +/* MUST come first to satisfy core headers like ogs-list.h */ +#include "core/ogs-core.h" + +/* App layer (logging domain, etc.) */ #include "app/ogs-app.h" +/* Expose internal metrics structures to metrics library users */ #define OGS_METRICS_INSIDE - #include "metrics/context.h" +#undef OGS_METRICS_INSIDE + #ifdef __cplusplus extern "C" { #endif -#undef OGS_METRICS_INSIDE +/* Already present for UEs */ +extern size_t (*ogs_metrics_connected_ues_dumper)(char *buf, size_t buflen); +void ogs_metrics_register_connected_ues(size_t (*fn)(char *buf, size_t buflen)); -extern int __ogs_metrics_domain; +/* New: gNBs dumper hook (AMF) */ +extern size_t (*ogs_metrics_connected_gnbs_dumper)(char *buf, size_t buflen); +void ogs_metrics_register_connected_gnbs(size_t (*fn)(char *buf, size_t buflen)); -#undef OGS_LOG_DOMAIN -#define OGS_LOG_DOMAIN __ogs_metrics_domain +/* New: eNBs dumper hook (AMF) */ +extern size_t (*ogs_metrics_connected_enbs_dumper)(char *buf, size_t buflen); +void ogs_metrics_register_connected_enbs(size_t (*fn)(char *buf, size_t buflen)); #ifdef __cplusplus } #endif +#undef OGS_LOG_DOMAIN +#define OGS_LOG_DOMAIN __ogs_metrics_domain + +#ifdef __cplusplus +} /* extern "C" */ +#endif + #endif /* OGS_METRICS_H */ + diff --git a/lib/metrics/prometheus/context.c b/lib/metrics/prometheus/context.c index 9972c2892..b68106157 100644 --- a/lib/metrics/prometheus/context.c +++ b/lib/metrics/prometheus/context.c @@ -1,6 +1,7 @@ /* * Copyright (C) 2022 by sysmocom - s.f.m.c. GmbH * Copyright (C) 2023 by Sukchan Lee + * Copyright (C) 2025 by Juraj Elias * * This file is part of Open5GS. * @@ -18,21 +19,37 @@ * along with this program. If not, see . */ -#include "ogs-metrics.h" + /* + * Prometheus HTTP server (MicroHTTPD) with optional JSON endpoints: + * - / (provide health check) + * - /metrics (provide prometheus metrics metrics according to the relevant NF) + * - /connected-ues (provided by NF registering ogs_metrics_connected_ues_dumper) + * - /connected-gnbs (provided by NF registering ogs_metrics_connected_gnbs_dumper) + * - /connected-enbs (provided by NF registering ogs_metrics_connected_enbs_dumper) + */ + +#include "ogs-core.h" +#include "metrics/ogs-metrics.h" #include /* AI_PASSIVE */ #include "prom.h" #include "microhttpd.h" +#include +extern int __ogs_metrics_domain; #define MAX_LABELS 8 +#if MHD_VERSION >= 0x00096100 +static void free_callback(void *cls) { ogs_free(cls); } +#endif + typedef struct ogs_metrics_server_s { ogs_socknode_t node; struct MHD_Daemon *mhd; } ogs_metrics_server_t; typedef struct ogs_metrics_spec_s { - ogs_metrics_context_t *ctx; /* backpointer */ + ogs_metrics_context_t *ctx; /* backpointer */ ogs_list_t entry; /* included in ogs_metrics_context_t */ ogs_metrics_metric_type_t type; char *name; @@ -54,6 +71,7 @@ typedef struct ogs_metrics_inst_s { static OGS_POOL(metrics_spec_pool, ogs_metrics_spec_t); static OGS_POOL(metrics_server_pool, ogs_metrics_server_t); +/* Forward decls */ static int ogs_metrics_context_server_start(ogs_metrics_server_t *server); static int ogs_metrics_context_server_stop(ogs_metrics_server_t *server); @@ -66,7 +84,6 @@ void ogs_metrics_server_init(ogs_metrics_context_t *ctx) void ogs_metrics_server_open(ogs_metrics_context_t *ctx) { ogs_metrics_server_t *server = NULL; - ogs_list_for_each(&ctx->server_list, server) ogs_metrics_context_server_start(server); } @@ -74,35 +91,33 @@ void ogs_metrics_server_open(ogs_metrics_context_t *ctx) void ogs_metrics_server_close(ogs_metrics_context_t *ctx) { ogs_metrics_server_t *server = NULL, *next = NULL; - ogs_list_for_each_safe(&ctx->server_list, next, server) ogs_metrics_context_server_stop(server); } void ogs_metrics_server_final(ogs_metrics_context_t *ctx) { - ogs_metrics_server_remove_all(); + ogs_metrics_server_t *server = NULL, *next = NULL; + + ogs_list_for_each_safe(&ctx->server_list, next, server) + ogs_metrics_server_remove(server); ogs_pool_final(&metrics_server_pool); } -ogs_metrics_server_t *ogs_metrics_server_add( - ogs_sockaddr_t *addr, ogs_sockopt_t *option) +ogs_metrics_server_t *ogs_metrics_server_add(ogs_sockaddr_t *addr, ogs_sockopt_t *option) { ogs_metrics_server_t *server = NULL; ogs_assert(addr); - ogs_pool_alloc(&metrics_server_pool, &server); ogs_assert(server); - memset(server, 0, sizeof(ogs_metrics_server_t)); + memset(server, 0, sizeof *server); ogs_assert(OGS_OK == ogs_copyaddrinfo(&server->node.addr, addr)); - if (option) - server->node.option = ogs_memdup(option, sizeof *option); + if (option) server->node.option = ogs_memdup(option, sizeof *option); ogs_list_add(&ogs_metrics_self()->server_list, server); - return server; } @@ -114,67 +129,47 @@ void ogs_metrics_server_remove(ogs_metrics_server_t *server) ogs_assert(server->node.addr); ogs_freeaddrinfo(server->node.addr); - if (server->node.option) - ogs_free(server->node.option); + if (server->node.option) ogs_free(server->node.option); ogs_pool_free(&metrics_server_pool, server); } -void ogs_metrics_server_remove_all(void) -{ - ogs_metrics_server_t *server = NULL, *next_server = NULL; - - ogs_list_for_each_safe( - &ogs_metrics_self()->server_list, next_server, server) { - ogs_metrics_server_remove(server); - } -} - static void mhd_server_run(short when, ogs_socket_t fd, void *data) { + (void)when; (void)fd; struct MHD_Daemon *mhd_daemon = data; - ogs_assert(mhd_daemon); MHD_run(mhd_daemon); } static void mhd_server_notify_connection(void *cls, - struct MHD_Connection *connection, - void **socket_context, + struct MHD_Connection *connection, void **socket_context, enum MHD_ConnectionNotificationCode toe) { + (void)cls; struct MHD_Daemon *mhd_daemon = NULL; MHD_socket mhd_socket = INVALID_SOCKET; const union MHD_ConnectionInfo *mhd_info = NULL; - struct { - ogs_poll_t *read; - } poll; + struct { ogs_poll_t *read; } poll = {0}; switch (toe) { - case MHD_CONNECTION_NOTIFY_STARTED: - mhd_info = MHD_get_connection_info( - connection, MHD_CONNECTION_INFO_DAEMON); - ogs_assert(mhd_info); - mhd_daemon = mhd_info->daemon; - ogs_assert(mhd_daemon); + case MHD_CONNECTION_NOTIFY_STARTED: + mhd_info = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_DAEMON); + ogs_assert(mhd_info); mhd_daemon = mhd_info->daemon; ogs_assert(mhd_daemon); - mhd_info = MHD_get_connection_info( - connection, MHD_CONNECTION_INFO_CONNECTION_FD); - ogs_assert(mhd_info); - mhd_socket = mhd_info->connect_fd; - ogs_assert(mhd_socket != INVALID_SOCKET); + mhd_info = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_CONNECTION_FD); + ogs_assert(mhd_info); mhd_socket = mhd_info->connect_fd; ogs_assert(mhd_socket != INVALID_SOCKET); - poll.read = ogs_pollset_add(ogs_app()->pollset, - OGS_POLLIN, mhd_socket, mhd_server_run, mhd_daemon); - ogs_assert(poll.read); - *socket_context = poll.read; - break; - case MHD_CONNECTION_NOTIFY_CLOSED: - poll.read = *socket_context; - if (poll.read) - ogs_pollset_remove(poll.read); - break; + poll.read = ogs_pollset_add(ogs_app()->pollset, OGS_POLLIN, mhd_socket, mhd_server_run, mhd_daemon); + ogs_assert(poll.read); + *socket_context = poll.read; + break; + + case MHD_CONNECTION_NOTIFY_CLOSED: + poll.read = *socket_context; + if (poll.read) ogs_pollset_remove(poll.read); + break; } } @@ -184,41 +179,142 @@ typedef enum MHD_Result _MHD_Result; typedef int _MHD_Result; #endif -static _MHD_Result mhd_server_access_handler(void *cls, struct MHD_Connection *connection, - const char *url, const char *method, const char *version, - const char *upload_data, size_t *upload_data_size, void **con_cls) { +/* Small helper to serve JSON from a registered dumper */ +static _MHD_Result serve_json_from_dumper(struct MHD_Connection *connection, + size_t (*dumper)(char*, size_t), + const char *missing_msg) +{ + if (!dumper) { + struct MHD_Response *rsp = MHD_create_response_from_buffer(strlen(missing_msg), + (void*)missing_msg, MHD_RESPMEM_PERSISTENT); + if (!rsp) return (_MHD_Result)MHD_NO; + int ret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, rsp); + MHD_destroy_response(rsp); + return (_MHD_Result)ret; + } + + size_t cap = 512 * 1024; + char *bufjson = (char *)ogs_malloc(cap); + if (!bufjson) { + const char *msg = "Out of memory\n"; + struct MHD_Response *rsp = MHD_create_response_from_buffer(strlen(msg), + (void*)msg, MHD_RESPMEM_PERSISTENT); + if (!rsp) return (_MHD_Result)MHD_NO; + int ret = MHD_queue_response(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, rsp); + MHD_destroy_response(rsp); + return (_MHD_Result)ret; + } + + size_t n = dumper(bufjson, cap); + if (n >= cap - 1) { + /* grow once */ + size_t newcap = cap * 2; + char *tmp = (char *)ogs_realloc(bufjson, newcap); + if (!tmp) { + ogs_free(bufjson); + const char *msg = "Out of memory\n"; + struct MHD_Response *rsp = MHD_create_response_from_buffer(strlen(msg), + (void*)msg, MHD_RESPMEM_PERSISTENT); + if (!rsp) return (_MHD_Result)MHD_NO; + int ret = MHD_queue_response(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, rsp); + MHD_destroy_response(rsp); + return (_MHD_Result)ret; + } + bufjson = tmp; cap = newcap; + n = dumper(bufjson, cap); + if (n >= cap - 1) { + /* graceful fallback */ + n = ogs_snprintf(bufjson, cap, "[]"); + } + } - const char *buf; struct MHD_Response *rsp; - int ret; +#if MHD_VERSION >= 0x00096100 + rsp = MHD_create_response_from_buffer_with_free_callback(n, (void*)bufjson, free_callback); + bufjson = NULL; /* ownership moved to MHD */ +#else + rsp = MHD_create_response_from_buffer(n, (void*)bufjson, MHD_RESPMEM_MUST_COPY); +#endif + if (!rsp) { +#if MHD_VERSION < 0x00096100 + ogs_free(bufjson); +#endif + return (_MHD_Result)MHD_NO; + } + MHD_add_response_header(rsp, "Content-Type", "application/json"); + MHD_add_response_header(rsp, "Access-Control-Allow-Origin", "*"); + int ret = MHD_queue_response(connection, MHD_HTTP_OK, rsp); + MHD_destroy_response(rsp); +#if MHD_VERSION < 0x00096100 + ogs_free(bufjson); +#endif + return (_MHD_Result)ret; +} + +static _MHD_Result +mhd_server_access_handler(void *cls, struct MHD_Connection *connection, + const char *url, const char *method, const char *version, + const char *upload_data, size_t *upload_data_size, void **con_cls) +{ + (void)cls; (void)version; (void)upload_data; (void)upload_data_size; (void)con_cls; + + const char *buf = NULL; + struct MHD_Response *rsp = NULL; + int ret = MHD_NO; + + /* Only GET is supported */ if (strcmp(method, "GET") != 0) { buf = "Invalid HTTP Method\n"; rsp = MHD_create_response_from_buffer(strlen(buf), (void *)buf, MHD_RESPMEM_PERSISTENT); ret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, rsp); MHD_destroy_response(rsp); - return ret; + return (_MHD_Result)ret; } + + /* Health */ if (strcmp(url, "/") == 0) { buf = "OK\n"; rsp = MHD_create_response_from_buffer(strlen(buf), (void *)buf, MHD_RESPMEM_PERSISTENT); ret = MHD_queue_response(connection, MHD_HTTP_OK, rsp); MHD_destroy_response(rsp); - return ret; + return (_MHD_Result)ret; } + + /* Prometheus metrics plain-text */ if (strcmp(url, "/metrics") == 0) { buf = prom_collector_registry_bridge(PROM_COLLECTOR_REGISTRY_DEFAULT); - rsp = MHD_create_response_from_buffer(strlen(buf), (void *)buf, MHD_RESPMEM_MUST_FREE); + rsp = MHD_create_response_from_buffer(strlen(buf), (void *)buf, MHD_RESPMEM_MUST_COPY); MHD_add_response_header(rsp, "Content-Type", "text/plain; version=0.0.4; charset=utf-8"); ret = MHD_queue_response(connection, MHD_HTTP_OK, rsp); MHD_destroy_response(rsp); - return ret; + return (_MHD_Result)ret; } + + /* JSON: connected UEs (SMF/MME/etc.) */ + if (strcmp(url, "/connected-ues") == 0) { + return serve_json_from_dumper(connection, ogs_metrics_connected_ues_dumper, + "connected-ues endpoint not available on this NF\n"); + } + + /* JSON: connected gNBs (AMF) */ + if (strcmp(url, "/connected-gnbs") == 0) { + return serve_json_from_dumper(connection, ogs_metrics_connected_gnbs_dumper, + "connected-gnbs endpoint not available on this NF\n"); + } + + /* JSON: connected eNBs (MME) */ + if (strcmp(url, "/connected-enbs") == 0) { + return serve_json_from_dumper(connection, ogs_metrics_connected_enbs_dumper, + "connected-enbs endpoint not available on this NF\n"); + } + + /* No matching route */ buf = "Bad Request\n"; rsp = MHD_create_response_from_buffer(strlen(buf), (void *)buf, MHD_RESPMEM_PERSISTENT); ret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, rsp); MHD_destroy_response(rsp); - return ret; + return (_MHD_Result)ret; } static int ogs_metrics_context_server_start(ogs_metrics_server_t *server) @@ -254,31 +350,26 @@ static int ogs_metrics_context_server_start(ogs_metrics_server_t *server) mhd_ops[index].option = MHD_OPTION_NOTIFY_CONNECTION; mhd_ops[index].value = (intptr_t)&mhd_server_notify_connection; - mhd_ops[index].ptr_value = NULL; - index++; + mhd_ops[index].ptr_value = NULL; index++; mhd_ops[index].option = MHD_OPTION_SOCK_ADDR; mhd_ops[index].value = 0; - mhd_ops[index].ptr_value = (void *)&addr->sa; - index++; + mhd_ops[index].ptr_value = (void *)&addr->sa; index++; mhd_ops[index].option = MHD_OPTION_END; mhd_ops[index].value = 0; - mhd_ops[index].ptr_value = NULL; - index++; + mhd_ops[index].ptr_value = NULL; index++; if (server->mhd) { ogs_error("Prometheus HTTP server is already opened!"); return OGS_ERROR; } - server->mhd = MHD_start_daemon( - mhd_flags, - 0, - NULL, NULL, - mhd_server_access_handler, server, - MHD_OPTION_ARRAY, mhd_ops, - MHD_OPTION_END); + server->mhd = MHD_start_daemon(mhd_flags, 0, + NULL, NULL, + mhd_server_access_handler, server, + MHD_OPTION_ARRAY, mhd_ops, + MHD_OPTION_END); if (!server->mhd) { ogs_error("Cannot start Prometheus HTTP server"); return OGS_ERROR; @@ -288,17 +379,15 @@ static int ogs_metrics_context_server_start(ogs_metrics_server_t *server) mhd_info = MHD_get_daemon_info(server->mhd, MHD_DAEMON_INFO_LISTEN_FD); ogs_assert(mhd_info); - server->node.poll = ogs_pollset_add(ogs_app()->pollset, - OGS_POLLIN, mhd_info->listen_fd, mhd_server_run, server->mhd); + server->node.poll = ogs_pollset_add(ogs_app()->pollset, OGS_POLLIN, + mhd_info->listen_fd, mhd_server_run, server->mhd); ogs_assert(server->node.poll); hostname = ogs_gethostname(addr); if (hostname) - ogs_info("metrics_server() [http://%s]:%d", - hostname, OGS_PORT(addr)); + ogs_info("metrics_server() [http://%s]:%d", hostname, OGS_PORT(addr)); else - ogs_info("metrics_server() [http://%s]:%d", - OGS_ADDR(addr, buf), OGS_PORT(addr)); + ogs_info("metrics_server() [http://%s]:%d", OGS_ADDR(addr, buf), OGS_PORT(addr)); return OGS_OK; } @@ -317,11 +406,12 @@ static int ogs_metrics_context_server_stop(ogs_metrics_server_t *server) return OGS_OK; } +/* ---- Metric spec/inst API (unchanged) ---------------------------------- */ + void ogs_metrics_spec_init(ogs_metrics_context_t *ctx) { ogs_list_init(&ctx->spec_list); ogs_pool_init(&metrics_spec_pool, ogs_app()->metrics.max_specs); - prom_collector_registry_default_init(); } @@ -329,11 +419,10 @@ void ogs_metrics_spec_final(ogs_metrics_context_t *ctx) { ogs_metrics_spec_t *spec = NULL, *next = NULL; - ogs_list_for_each_entry_safe(&ctx->spec_list, next, spec, entry) { + ogs_list_for_each_entry_safe(&ctx->spec_list, next, spec, entry) ogs_metrics_spec_free(spec); - } - prom_collector_registry_destroy(PROM_COLLECTOR_REGISTRY_DEFAULT); + prom_collector_registry_destroy(PROM_COLLECTOR_REGISTRY_DEFAULT); ogs_pool_final(&metrics_spec_pool); } @@ -345,8 +434,7 @@ ogs_metrics_spec_t *ogs_metrics_spec_new( { ogs_metrics_spec_t *spec; unsigned int i; - - prom_histogram_buckets_t *buckets; + prom_histogram_buckets_t *buckets = NULL; double *upper_bounds; ogs_assert(name); @@ -393,17 +481,14 @@ ogs_metrics_spec_t *ogs_metrics_spec_new( case OGS_METRICS_HISTOGRAM_BUCKET_TYPE_VARIABLE: buckets = (prom_histogram_buckets_t *)prom_malloc(sizeof(prom_histogram_buckets_t)); ogs_assert(buckets); - ogs_assert(histogram_params->count <= OGS_METRICS_HIST_VAR_BUCKETS_MAX); buckets->count = histogram_params->count; - upper_bounds = (double *)prom_malloc( - sizeof(double) * histogram_params->count); + upper_bounds = (double *)prom_malloc(sizeof(double) * histogram_params->count); ogs_assert(upper_bounds); for (i = 0; i < histogram_params->count; i++) { upper_bounds[i] = histogram_params->var.buckets[i]; - if (i > 0) - ogs_assert(upper_bounds[i] > upper_bounds[i - 1]); + if (i > 0) ogs_assert(upper_bounds[i] > upper_bounds[i - 1]); } buckets->upper_bounds = upper_bounds; break; @@ -432,9 +517,8 @@ void ogs_metrics_spec_free(ogs_metrics_spec_t *spec) ogs_list_remove(&spec->ctx->spec_list, &spec->entry); - ogs_list_for_each_entry_safe(&spec->inst_list, next, inst, entry) { + ogs_list_for_each_entry_safe(&spec->inst_list, next, inst, entry) ogs_metrics_inst_free(inst); - } ogs_free(spec->name); ogs_free(spec->description); @@ -444,7 +528,6 @@ void ogs_metrics_spec_free(ogs_metrics_spec_t *spec) ogs_pool_free(&metrics_spec_pool, spec); } - ogs_metrics_inst_t *ogs_metrics_inst_new( ogs_metrics_spec_t *spec, unsigned int num_labels, const char **label_values) @@ -455,7 +538,7 @@ ogs_metrics_inst_t *ogs_metrics_inst_new( ogs_assert(spec); ogs_assert(num_labels == spec->num_labels); - inst = ogs_calloc(1, sizeof(ogs_metrics_inst_t)); + inst = ogs_calloc(1, sizeof *inst); ogs_assert(inst); inst->spec = spec; inst->num_labels = num_labels; @@ -502,7 +585,6 @@ void ogs_metrics_inst_reset(ogs_metrics_inst_t *inst) prom_gauge_set(inst->spec->prom, (double)inst->spec->initial_val, (const char **)inst->label_values); break; default: - /* Other types have no way to reset */ break; } } @@ -529,3 +611,4 @@ void ogs_metrics_inst_add(ogs_metrics_inst_t *inst, int val) break; } } + diff --git a/src/amf/connected_gnbs.c b/src/amf/connected_gnbs.c new file mode 100644 index 000000000..eacef8f7d --- /dev/null +++ b/src/amf/connected_gnbs.c @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * JSON dumper for /connected-gnbs (AMF) + * Output (one item per connected gNB): + * [ + * { + * "gnb_id": 100, + * "plmn": "99970", + * "network": { + * "amf_name": "efire-amf0", + * "ngap_port": 38412 + * }, + * "ng": { + * "setup_success": true, + * "sctp": { + * "peer": "[192.168.168.100]:60110", + * "max_out_streams": 2, + * "next_ostream_id": 1 + * } + * }, + * "supported_ta_list": [ + * { + * "tac": "000001", + * "bplmns": [ + * { + * "plmn": "99970", + * "snssai": [ + * { + * "sst": 1, + * "sd": "ffffff" + * } + * ] + * } + * ] + * } + * ], + * "num_connected_ues": 1 + * } + * ] + */ + +/* + * Copyright (C) 2025 by Juraj Elias + * JSON dumper for /connected-gnbs (AMF) + */ + +#include +#include +#include +#include +#include + +#include "context.h" +#include "ogs-proto.h" +#include "ogs-core.h" + +/* Exported */ +size_t amf_dump_connected_gnbs(char *buf, size_t buflen); + +/* ------------------------- small helpers ------------------------- */ + +static inline size_t append_safe(char *buf, size_t off, size_t buflen, const char *fmt, ...) +{ + if (!buf || off == (size_t)-1 || off >= buflen) return (size_t)-1; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf + off, buflen - off, fmt, ap); + va_end(ap); + if (n < 0 || (size_t)n >= buflen - off) return (size_t)-1; + return off + (size_t)n; +} + +static size_t append_json_kv_escaped(char *buf, size_t off, size_t buflen, + const char *key, const char *val) +{ + if (!val) val = ""; + off = append_safe(buf, off, buflen, "\"%s\":\"", key); + if (off == (size_t)-1) return off; + for (const unsigned char *p = (const unsigned char *)val; *p; ++p) { + unsigned char c = *p; + if (c == '\\' || c == '\"') off = append_safe(buf, off, buflen, "\\%c", c); + else if (c < 0x20) off = append_safe(buf, off, buflen, "\\u%04x", (unsigned)c); + else off = append_safe(buf, off, buflen, "%c", c); + if (off == (size_t)-1) return off; + } + return append_safe(buf, off, buflen, "\""); +} + +/* "plmn":"XXXXX" */ +static size_t append_plmn_kv(char *buf, size_t off, size_t buflen, const ogs_plmn_id_t *plmn) +{ + char s[OGS_PLMNIDSTRLEN] = {0}; + ogs_plmn_id_to_string(plmn, s); + return append_safe(buf, off, buflen, "\"plmn\":\"%s\"", s); +} + +/* 24-bit helpers */ +static inline uint32_t u24_to_u32_portable(ogs_uint24_t v) +{ + uint32_t x = 0; memcpy(&x, &v, sizeof(v) < sizeof(x) ? sizeof(v) : sizeof(x)); + return (x & 0xFFFFFFu); +} + +static size_t append_u24_hex6(char *buf, size_t off, size_t buflen, const ogs_uint24_t v) +{ + uint32_t u = u24_to_u32_portable(v); + return append_safe(buf, off, buflen, "\"%06x\"", (unsigned)(u & 0xFFFFFFu)); +} + +/* S-NSSAI */ +static size_t append_snssai_obj(char *buf, size_t off, size_t buflen, const ogs_s_nssai_t *sn) +{ + unsigned sst = (unsigned)sn->sst; + uint32_t sd_u32 = u24_to_u32_portable(sn->sd); + off = append_safe(buf, off, buflen, "{"); + off = append_safe(buf, off, buflen, "\"sst\":%u", sst); + off = append_safe(buf, off, buflen, ",\"sd\":\"%06x\"}", (unsigned)(sd_u32 & 0xFFFFFFu)); + return off; +} + +static size_t append_snssai_arr(char *buf, size_t off, size_t buflen, + const ogs_s_nssai_t *arr, int n) +{ + off = append_safe(buf, off, buflen, "["); + for (int i = 0; i < n; i++) { + if (i) off = append_safe(buf, off, buflen, ","); + off = append_snssai_obj(buf, off, buflen, &arr[i]); + } + off = append_safe(buf, off, buflen, "]"); + return off; +} + +/* sockaddr -> string */ +static inline const char *safe_sa_str(const ogs_sockaddr_t *sa) +{ + if (!sa) return ""; + int fam = ((const struct sockaddr *)&sa->sa)->sa_family; + if (fam != AF_INET && fam != AF_INET6) return ""; + return ogs_sockaddr_to_string_static((ogs_sockaddr_t *)sa); +} + +/* UE counter on this gNB */ +static size_t count_connected_ues_for_gnb(const amf_gnb_t *gnb) +{ + size_t total = 0; ran_ue_t *r = NULL; + ogs_list_for_each(&((amf_gnb_t*)gnb)->ran_ue_list, r) total++; + return total; +} + +#define APPF(...) do { off = append_safe(buf, off, buflen, __VA_ARGS__); if (off==(size_t)-1) goto trunc; } while(0) +#define APPX(expr) do { off = (expr); if (off==(size_t)-1) goto trunc; } while(0) + +/* --------------------------- main --------------------------- */ + +size_t amf_dump_connected_gnbs(char *buf, size_t buflen) +{ + size_t off = 0; + amf_context_t *ctxt = amf_self(); + ogs_assert(ctxt); + amf_gnb_t *gnb = NULL; + + APPF("["); + bool first_gnb = true; + + ogs_list_for_each(&ctxt->gnb_list, gnb) { + if (!first_gnb) APPF(","); + first_gnb = false; + + size_t num_total = count_connected_ues_for_gnb(gnb); + + APPF("{"); + + APPF("\"gnb_id\":%u", (unsigned)gnb->gnb_id); + APPF(","); + APPX(append_plmn_kv(buf, off, buflen, &gnb->plmn_id)); + + APPF(",\"network\":{"); + APPX(append_json_kv_escaped(buf, off, buflen, "amf_name", ctxt->amf_name ? ctxt->amf_name : "")); + APPF(",\"ngap_port\":%u", (unsigned)ctxt->ngap_port); + APPF("}"); + + APPF(",\"ng\":{"); + APPF("\"setup_success\":%s", gnb->state.ng_setup_success ? "true" : "false"); + APPF(",\"sctp\":{"); + APPF("\"peer\":\"%s\"", safe_sa_str(gnb->sctp.addr)); + APPF(",\"max_out_streams\":%d", gnb->max_num_of_ostreams); + APPF(",\"next_ostream_id\":%u", (unsigned)gnb->ostream_id); + APPF("}"); + APPF("}"); + + APPF(",\"supported_ta_list\":["); + for (int t = 0; t < gnb->num_of_supported_ta_list; t++) { + if (t) APPF(","); + APPF("{"); + APPF("\"tac\":"); + APPX(append_u24_hex6(buf, off, buflen, gnb->supported_ta_list[t].tac)); + + APPF(",\"bplmns\":["); + for (int p = 0; p < gnb->supported_ta_list[t].num_of_bplmn_list; p++) { + if (p) APPF(","); + const ogs_plmn_id_t *bp_plmn = &gnb->supported_ta_list[t].bplmn_list[p].plmn_id; + const int ns = gnb->supported_ta_list[t].bplmn_list[p].num_of_s_nssai; + const ogs_s_nssai_t *sn = gnb->supported_ta_list[t].bplmn_list[p].s_nssai; + + APPF("{"); + APPX(append_plmn_kv(buf, off, buflen, bp_plmn)); + APPF(",\"snssai\":"); + APPX(append_snssai_arr(buf, off, buflen, sn, ns)); + APPF("}"); + } + APPF("]"); + APPF("}"); + } + APPF("]"); + + APPF(",\"num_connected_ues\":%zu", num_total); + + APPF("}"); + } + + APPF("]"); + return off; + +trunc: + if (buf && buflen >= 3) { buf[0]='['; buf[1]=']'; buf[2]='\0'; return 2; } + if (buf && buflen) buf[0]='\0'; + return 0; +} + diff --git a/src/amf/connected_gnbs.h b/src/amf/connected_gnbs.h new file mode 100644 index 000000000..8cc67dad5 --- /dev/null +++ b/src/amf/connected_gnbs.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * /connected-gnbs — AMF-side JSON exporter (Prometheus HTTP endpoint) + * + */ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* JSON dumper for /connected-gnbs. + * Returns number of bytes written (<= buflen-1), buffer is always NUL-terminated. + */ +size_t amf_dump_connected_gnbs(char *buf, size_t buflen); + +#ifdef __cplusplus +} +#endif + diff --git a/src/amf/init.c b/src/amf/init.c index 864c21519..da354a3dd 100644 --- a/src/amf/init.c +++ b/src/amf/init.c @@ -21,6 +21,9 @@ #include "ngap-path.h" #include "metrics.h" +#include "ogs-metrics.h" +#include "connected_gnbs.h" + static ogs_thread_t *thread; static void amf_main(void *data); static int initialized = 0; @@ -55,6 +58,7 @@ int amf_initialize(void) if (rv != OGS_OK) return rv; ogs_metrics_context_open(ogs_metrics_self()); + ogs_metrics_register_connected_gnbs(amf_dump_connected_gnbs); rv = amf_sbi_open(); if (rv != OGS_OK) return rv; diff --git a/src/amf/meson.build b/src/amf/meson.build index 88ab0bc44..c7f98b748 100644 --- a/src/amf/meson.build +++ b/src/amf/meson.build @@ -61,6 +61,7 @@ libamf_sources = files(''' init.c metrics.c + connected_gnbs.c '''.split()) libamf = static_library('amf', diff --git a/src/mme/connected_enbs.c b/src/mme/connected_enbs.c new file mode 100644 index 000000000..da272853e --- /dev/null +++ b/src/mme/connected_enbs.c @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * JSON dumper for /connected-enbs (MME) + * Output (one item per connected eNB): + *[ + * { + * "enb_id": 264040, + * "plmn": "99970", + * "network": { + * "mme_name": "efire-mme0" + * }, + * "s1": { + * "setup_success": true, + * "sctp": { + * "peer": "[192.168.168.254]:36412", + * "max_out_streams": 10, + * "next_ostream_id": 1 + * } + * }, + * "supported_ta_list": [ + * { + * "tac": "000001", + * "plmn": "99970" + * } + * ], + * "num_connected_ues": 1 + * } + *] + */ + +#include +#include +#include +#include + +#include "ogs-core.h" +#include "ogs-proto.h" +#include "ogs-app.h" +#include "mme-context.h" + +/* Exported */ +size_t mme_dump_connected_enbs(char *buf, size_t buflen); + +/* ------------------------- small helpers ------------------------- */ + +static inline size_t append_safe(char *buf, size_t off, size_t buflen, const char *fmt, ...) +{ + if (!buf || off == (size_t)-1 || off >= buflen) return (size_t)-1; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf + off, buflen - off, fmt, ap); + va_end(ap); + if (n < 0 || (size_t)n >= buflen - off) return (size_t)-1; + return off + (size_t)n; +} + +static size_t append_json_kv_escaped(char *buf, size_t off, size_t buflen, + const char *key, const char *val) +{ + if (!val) val = ""; + off = append_safe(buf, off, buflen, "\"%s\":\"", key); + if (off == (size_t)-1) return off; + for (const unsigned char *p = (const unsigned char *)val; *p; ++p) { + unsigned char c = *p; + if (c == '\\' || c == '\"') off = append_safe(buf, off, buflen, "\\%c", c); + else if (c < 0x20) off = append_safe(buf, off, buflen, "\\u%04x", (unsigned)c); + else off = append_safe(buf, off, buflen, "%c", c); + if (off == (size_t)-1) return off; + } + return append_safe(buf, off, buflen, "\""); +} + +/* "plmn":"XXXXX" */ +static size_t append_plmn_kv(char *buf, size_t off, size_t buflen, const ogs_plmn_id_t *plmn) +{ + char s[OGS_PLMNIDSTRLEN] = {0}; + ogs_plmn_id_to_string(plmn, s); + return append_safe(buf, off, buflen, "\"plmn\":\"%s\"", s); +} + +static size_t append_u24_hex6_str(char *buf, size_t off, size_t buflen, uint32_t v24) +{ + return append_safe(buf, off, buflen, "\"%06X\"", (unsigned)(v24 & 0xFFFFFF)); +} + +static inline const char *safe_sa_str(const ogs_sockaddr_t *sa) +{ + if (!sa) return ""; + int fam = ((const struct sockaddr *)&sa->sa)->sa_family; + if (fam != AF_INET && fam != AF_INET6) return ""; + return ogs_sockaddr_to_string_static((ogs_sockaddr_t *)sa); +} + +#define APPF(...) do { off = append_safe(buf, off, buflen, __VA_ARGS__); if (off==(size_t)-1) goto trunc; } while(0) +#define APPX(expr) do { off = (expr); if (off==(size_t)-1) goto trunc; } while(0) + +/* ------------------------------- main ------------------------------- */ + +size_t mme_dump_connected_enbs(char *buf, size_t buflen) +{ + size_t off = 0; + if (!buf || buflen == 0) return 0; + + mme_context_t *ctxt = mme_self(); + if (!ctxt) { + APPF("[]"); + return off; + } + + APPF("["); + bool first = true; + + mme_enb_t *enb = NULL; + ogs_list_for_each(&ctxt->enb_list, enb) { + if (!first) APPF(","); + first = false; + + size_t num_connected_ues = 0; + { + enb_ue_t *ue = NULL; + ogs_list_for_each(&enb->enb_ue_list, ue) num_connected_ues++; + } + + APPF("{"); + + APPF("\"enb_id\":%u", (unsigned)enb->enb_id); + APPF(","); + APPX(append_plmn_kv(buf, off, buflen, &enb->plmn_id)); + + APPF(",\"network\":{"); + APPX(append_json_kv_escaped(buf, off, buflen, "mme_name", + ctxt->mme_name ? ctxt->mme_name : "")); + APPF("}"); + + APPF(",\"s1\":{"); + APPF("\"setup_success\":%s", enb->state.s1_setup_success ? "true" : "false"); + APPF(",\"sctp\":{"); + APPF("\"peer\":\"%s\"", safe_sa_str(enb->sctp.addr)); + APPF(",\"max_out_streams\":%d", enb->max_num_of_ostreams); + APPF(",\"next_ostream_id\":%u", (unsigned)enb->ostream_id); + APPF("}"); + APPF("}"); + + APPF(",\"supported_ta_list\":["); + for (int t = 0; t < enb->num_of_supported_ta_list; t++) { + if (t) APPF(","); + APPF("{"); + APPF("\"tac\":"); + APPX(append_u24_hex6_str(buf, off, buflen, enb->supported_ta_list[t].tac)); + APPF(",\"plmn\":"); + APPX(append_plmn_kv(buf, off, buflen, &enb->supported_ta_list[t].plmn_id)); + APPF("}"); + } + APPF("]"); + + APPF(",\"num_connected_ues\":%zu", num_connected_ues); + + APPF("}"); + } + + APPF("]"); + return off; + +trunc: + if (buf && buflen >= 3) { buf[0]='['; buf[1]=']'; buf[2]='\0'; return 2; } + if (buf && buflen) buf[0]='\0'; + return 0; +} + diff --git a/src/mme/connected_enbs.h b/src/mme/connected_enbs.h new file mode 100644 index 000000000..1f29599ca --- /dev/null +++ b/src/mme/connected_enbs.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/* + * /connected-gnbs — AMF-side JSON exporter (Prometheus HTTP endpoint) + * + * License: AGPLv3+ + */ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* JSON dumper for /connected-gnbs. + * Returns number of bytes written (<= buflen-1), buffer is always NUL-terminated. + */ +size_t mme_dump_connected_enbs(char *buf, size_t buflen); + +#ifdef __cplusplus +} +#endif + diff --git a/src/mme/meson.build b/src/mme/meson.build index 862d30992..313821827 100644 --- a/src/mme/meson.build +++ b/src/mme/meson.build @@ -79,6 +79,7 @@ libmme_sources = files(''' mme-path.c sbc-handler.c metrics.c + connected_enbs.c '''.split()) libmme = static_library('mme', diff --git a/src/mme/mme-init.c b/src/mme/mme-init.c index 252456c67..ba0b7f275 100644 --- a/src/mme/mme-init.c +++ b/src/mme/mme-init.c @@ -30,6 +30,7 @@ #include "sgsap-path.h" #include "mme-gtp-path.h" #include "metrics.h" +#include "connected_enbs.h" static ogs_thread_t *thread; static void mme_main(void *data); @@ -66,6 +67,7 @@ int mme_initialize(void) if (rv != OGS_OK) return rv; ogs_metrics_context_open(ogs_metrics_self()); + ogs_metrics_register_connected_enbs(mme_dump_connected_enbs); rv = mme_fd_init(); if (rv != OGS_OK) return OGS_ERROR; diff --git a/src/smf/connected_ues.c b/src/smf/connected_ues.c new file mode 100644 index 000000000..a88d66cd2 --- /dev/null +++ b/src/smf/connected_ues.c @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * Connected UEs JSON dumper for the Prometheus HTTP server (/connected-ues). + * - 5G PDUs: psi+dnn, snssai, qos_flows [{qfi,5qi}], pdu_state ("active"/"inactive"/"unknown") + * - LTE PDUs: ebi(+psi if non-zero)+apn, qos_flows [{ebi,qci}], pdu_state ("unknown" at SMF scope) + * - UE-level: ue_activity ("active" if any PDU active; "unknown" if none active but any unknown; else "idle") + */ + +/* + * JSON dumper for /connected-gnbs (AMF) + * Output (one item per connected gNB): + * [ + * { + * "supi": "imsi-999700000083810", + * "pdu": [ + * { + * "psi": 1, + * "dnn": "internet", + * "ipv4": "10.45.0.2", + * "snssai": { + * "sst": 1, + * "sd": "ffffff" + * }, + * "qos_flows": [ + * { + * "qfi": 1, + * "5qi": 9 + * } + * ], + * "pdu_state": "inactive" + * } + * ], + * "ue_activity": "idle" + * }, + * ] + */ +/* + * Copyright (C) 2025 by Juraj Elias + * This file is part of Open5GS (AGPLv3+) + * + * JSON dumper for /connected-ues (SMF) + * - 5G PDUs: psi+dnn, snssai, qos_flows [{qfi,5qi}], pdu_state + * - LTE PDUs: ebi(+psi if non-zero)+apn, qos_flows [{ebi,qci}], pdu_state="unknown" + * - UE-level: ue_activity derived from PDU states + */ + +#include +#include +#include +#include +#include + +#include "ogs-core.h" /* OGS_INET_NTOP, OGS_INET6_NTOP, OGS_ADDRSTRLEN, ogs_uint24_t */ +#include "context.h" /* smf_self(), smf_ue_t, smf_sess_t, smf_bearer_t, ogs_s_nssai_t */ +#include "connected_ues.h" /* size_t smf_dump_connected_ues(char *buf, size_t buflen) */ + +/* ------------------------- small helpers ------------------------- */ + +static inline size_t append_safe(char *buf, size_t off, size_t buflen, const char *fmt, ...) +{ + if (!buf || off == (size_t)-1 || off >= buflen) return (size_t)-1; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf + off, buflen - off, fmt, ap); + va_end(ap); + if (n < 0 || (size_t)n >= buflen - off) return (size_t)-1; + return off + (size_t)n; +} + +/* Escapes \" \\ and control chars, emits: "key":"escaped" */ +static size_t append_json_kv_escaped(char *buf, size_t off, size_t buflen, + const char *key, const char *val) +{ + if (!val) val = ""; + off = append_safe(buf, off, buflen, "\"%s\":\"", key); + if (off == (size_t)-1) return off; + + for (const unsigned char *p = (const unsigned char *)val; *p; ++p) { + unsigned char c = *p; + if (c == '\\' || c == '\"') { + off = append_safe(buf, off, buflen, "\\%c", c); + } else if (c < 0x20) { + off = append_safe(buf, off, buflen, "\\u%04x", (unsigned)c); + } else { + off = append_safe(buf, off, buflen, "%c", c); + } + if (off == (size_t)-1) return off; + } + return append_safe(buf, off, buflen, "\""); +} + +/* 24-bit helpers */ +static inline uint32_t u24_to_u32_portable(ogs_uint24_t v) +{ + uint32_t x = 0; + memcpy(&x, &v, sizeof(v) < sizeof(x) ? sizeof(v) : sizeof(x)); + return (x & 0xFFFFFFu); +} + +/* Emit a S-NSSAI object */ +static size_t append_snssai_obj(char *buf, size_t off, size_t buflen, const ogs_s_nssai_t *sn) +{ + unsigned sst = (unsigned)sn->sst; + uint32_t sd_u32 = u24_to_u32_portable(sn->sd); + off = append_safe(buf, off, buflen, "{"); + off = append_safe(buf, off, buflen, "\"sst\":%u", sst); + off = append_safe(buf, off, buflen, ",\"sd\":\"%06x\"}", (unsigned)(sd_u32 & 0xFFFFFFu)); + return off; +} + +/* ------------------------- state helpers ------------------------- */ + +static inline int up_state_of(const smf_sess_t *s) { + if (!s) return 0; + int u = (int)s->up_cnx_state; + if (u == 0) u = (int)s->nsmf_param.up_cnx_state; + return u; +} + +static inline bool has_n3_teid(const smf_sess_t *s) { + return s && (s->remote_ul_teid != 0U || s->remote_dl_teid != 0U); +} + +static inline bool bearer_list_has_qfi(const smf_sess_t *s) { + if (!s) return false; + smf_bearer_t *b = NULL; + ogs_list_for_each(&((smf_sess_t *)s)->bearer_list, b) { + if (b && b->qfi > 0) return true; + } + return false; +} + +/* Looks-5G heuristic: S-NSSAI present or any QFI bearer */ +static inline bool looks_5g_sess(const smf_sess_t *s) { + if (!s) return false; + if (s->s_nssai.sst != 0) return true; + if (u24_to_u32_portable(s->s_nssai.sd) != 0) return true; + if (bearer_list_has_qfi(s)) return true; + return false; +} + +/* 5G PDU state */ +static const char *pdu_state_from_5g(const smf_sess_t *sess) +{ + if (!sess) return "unknown"; + if ((int)sess->resource_status == (int)OpenAPI_resource_status_RELEASED) + return "inactive"; + if (up_state_of(sess) == (int)OpenAPI_up_cnx_state_DEACTIVATED) + return "inactive"; + if (sess->n1_released || sess->n2_released) + return "inactive"; + if (!has_n3_teid(sess)) + return "inactive"; + return "active"; +} + +/* LTE/EPC PDU state at SMF scope: unknown */ +static const char *pdu_state_from_4g(const smf_sess_t *sess) +{ + (void)sess; + return "unknown"; +} + +/* QoS emitters */ +static size_t append_qos_info_5g(char *buf, size_t off, size_t buflen, const smf_sess_t *sess) +{ + smf_bearer_t *b = NULL; + bool first = true; + off = append_safe(buf, off, buflen, ",\"qos_flows\":["); + ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b) { + if (!b || b->qfi == 0) continue; + if (!first) off = append_safe(buf, off, buflen, ","); + first = false; + off = append_safe(buf, off, buflen, "{"); + off = append_safe(buf, off, buflen, "\"qfi\":%u", (unsigned)b->qfi); + if (b->qos.index > 0) + off = append_safe(buf, off, buflen, ",\"5qi\":%u", (unsigned)b->qos.index); + off = append_safe(buf, off, buflen, "}"); + if (off == (size_t)-1) break; + } + off = append_safe(buf, off, buflen, "]"); + return off; +} + +static size_t append_qos_info_4g(char *buf, size_t off, size_t buflen, const smf_sess_t *sess) +{ + smf_bearer_t *b = NULL; + bool first = true; + off = append_safe(buf, off, buflen, ",\"qos_flows\":["); + ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b) { + if (!b || b->ebi == 0) continue; + if (!first) off = append_safe(buf, off, buflen, ","); + first = false; + + unsigned qci_val = (unsigned)b->qos.index; + if (qci_val == 0 && sess) qci_val = (unsigned)sess->session.qos.index; + + off = append_safe(buf, off, buflen, "{"); + off = append_safe(buf, off, buflen, "\"ebi\":%u", (unsigned)b->ebi); + if (qci_val > 0) + off = append_safe(buf, off, buflen, ",\"qci\":%u", qci_val); + off = append_safe(buf, off, buflen, "}"); + if (off == (size_t)-1) break; + } + off = append_safe(buf, off, buflen, "]"); + return off; +} + +/* Macros for safe appends */ +#define APPF(...) do { off = append_safe(buf, off, buflen, __VA_ARGS__); if (off==(size_t)-1) goto trunc; } while(0) +#define APPX(expr) do { off = (expr); if (off==(size_t)-1) goto trunc; } while(0) + +/* ------------------------------- main ------------------------------- */ + +size_t smf_dump_connected_ues(char *buf, size_t buflen) +{ + size_t off = 0; + if (!buf || buflen == 0) return 0; + + APPF("["); + bool first_ue = true; + + smf_ue_t *ue = NULL; + ogs_list_for_each(&smf_self()->smf_ue_list, ue) { + if (!ue) continue; + + if (!first_ue) APPF(","); + first_ue = false; + + bool any_active = false, any_unknown = false; + + APPF("{"); + + /* UE identity */ + const char *id = (ue->supi && ue->supi[0]) ? ue->supi : + (ue->imsi_bcd[0] ? ue->imsi_bcd : ""); + APPX(append_json_kv_escaped(buf, off, buflen, "supi", id)); + + /* PDU array */ + APPF(",\"pdu\":["); + bool first_pdu = true; + + smf_sess_t *sess = NULL; + ogs_list_for_each(&ue->sess_list, sess) { + if (!sess) continue; + const bool is_5g = looks_5g_sess(sess); + const char *pstate = is_5g ? pdu_state_from_5g(sess) + : pdu_state_from_4g(sess); + + if (!first_pdu) APPF(","); + first_pdu = false; + + APPF("{"); + + if (is_5g) { + /* 5G: PSI + DNN */ + APPF("\"psi\":%u,", (unsigned)sess->psi); + APPX(append_json_kv_escaped(buf, off, buflen, "dnn", + (sess->session.name ? sess->session.name : ""))); + } else { + /* LTE: PSI if non-zero, EBI root + APN */ + unsigned ebi_root = 0; + smf_bearer_t *b0 = NULL; + ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b0) { + if (b0 && b0->ebi > 0) { ebi_root = (unsigned)b0->ebi; break; } + } + if (sess->psi > 0) APPF("\"psi\":%u,", (unsigned)sess->psi); + APPF("\"ebi\":%u,", ebi_root); + APPX(append_json_kv_escaped(buf, off, buflen, "apn", + (sess->session.name ? sess->session.name : ""))); + } + + /* IPs if present */ + char ip4[OGS_ADDRSTRLEN] = ""; + char ip6[OGS_ADDRSTRLEN] = ""; + if (sess->ipv4) OGS_INET_NTOP(&sess->ipv4->addr, ip4); + if (sess->ipv6) OGS_INET6_NTOP(&sess->ipv6->addr, ip6); + if (ip4[0]) { APPF(","); APPX(append_json_kv_escaped(buf, off, buflen, "ipv4", ip4)); } + if (ip6[0]) { APPF(","); APPX(append_json_kv_escaped(buf, off, buflen, "ipv6", ip6)); } + + if (is_5g) { + /* S-NSSAI */ + APPF(",\"snssai\":"); + APPX(append_snssai_obj(buf, off, buflen, &sess->s_nssai)); + /* QoS flows */ + APPX(append_qos_info_5g(buf, off, buflen, sess)); + } else { + /* LTE QoS */ + APPX(append_qos_info_4g(buf, off, buflen, sess)); + } + + APPF(",\"pdu_state\":\"%s\"", pstate); + APPF("}"); + + if (strcmp(pstate, "active") == 0) any_active = true; + else if (strcmp(pstate, "unknown") == 0) any_unknown = true; + } + APPF("]"); + + const char *ue_act = any_active ? "active" : (any_unknown ? "unknown" : "idle"); + APPF(",\"ue_activity\":\"%s\"", ue_act); + + APPF("}"); + } + + APPF("]"); + return off; + +trunc: + /* Minimal valid JSON on overflow */ + if (buf && buflen >= 3) { buf[0]='['; buf[1]=']'; buf[2]='\0'; return 2; } + if (buf && buflen) buf[0]='\0'; + return 0; +} + diff --git a/src/smf/connected_ues.h b/src/smf/connected_ues.h new file mode 100644 index 000000000..c7bfe4eb2 --- /dev/null +++ b/src/smf/connected_ues.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * Minimal public API for connected_ues + */ +#ifndef SMF_CONNECTED_UES_H +#define SMF_CONNECTED_UES_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Fills buf with a compact JSON array. + * Returns the number of bytes written (excluding the terminating NUL). */ +size_t smf_dump_connected_ues(char *buf, size_t buflen); + +#ifdef __cplusplus +} +#endif + +#endif /* SMF_CONNECTED_UES_H */ + + diff --git a/src/smf/init.c b/src/smf/init.c index 83b7a4f41..b359ab91e 100644 --- a/src/smf/init.c +++ b/src/smf/init.c @@ -23,6 +23,8 @@ #include "pfcp-path.h" #include "sbi-path.h" #include "metrics.h" +#include "ogs-metrics.h" /* for ogs_metrics_register_connected_ues */ +#include "connected_ues.h" /* declare smf_dump_connected_ues() */ static ogs_thread_t *thread; static void smf_main(void *data); @@ -90,6 +92,8 @@ int smf_initialize(void) thread = ogs_thread_create(smf_main, NULL); if (!thread) return OGS_ERROR; + ogs_metrics_register_connected_ues(smf_dump_connected_ues); + initialized = 1; return OGS_OK; diff --git a/src/smf/meson.build b/src/smf/meson.build index aff3dd3a4..0f5d472d1 100644 --- a/src/smf/meson.build +++ b/src/smf/meson.build @@ -70,6 +70,7 @@ libsmf_sources = files(''' ngap-path.h local-path.h metrics.h + connected_ues.h init.c event.c @@ -112,6 +113,7 @@ libsmf_sources = files(''' ngap-path.c local-path.c metrics.c + connected_ues.c '''.split()) libsmf = static_library('smf',