mirror of
https://github.com/open5gs/open5gs.git
synced 2025-11-01 20:44:06 +00:00
Open5GS connected UEs, APN/DNN, IP addresses (#4044)
Added additional fields: snssai, qos flow, pdu, and UE state. For 5G (for LTE, the pdu state is currently unknown). curl -s http://127.0.0.4:9090/connected-ues |jq . { "supi": "imsi-999700000083810", // 5G RAT "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" }, { "supi": "001010000056492", // LTE RAT "pdu": [ { "ebi": 5, "apn": "internet", "ipv4": "10.45.0.3", "qos_flows": [ { "ebi": 5, "qci": 9 } ], "pdu_state": "unknown" } ], "ue_activity": "unknown" }, Added other outputs related to the connected gNBs/eNBs to AMF and MME, so we should have the basic tools for the 4G/5G core operation. curl -s http://127.0.0.4:9090/connected-ues |jq . curl -s http://127.0.0.5:9090/connected-gnbs | jq . curl -s http://127.0.0.2:9090/connected-enb |jq . curl -s http://127.0.0.5:9090/connected-gnbs |jq . [ { "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" } ] }, { "plmn": "99971", "snssai": [ { "sst": 2, "sd": "000000" } ] } ] }, { "tac": "000051", "bplmns": [ { "plmn": "00101", "snssai": [ { "sst": 2, "sd": "123456" } ] } ] }, ], "num_connected_ues": 0 } ] curl -s http://127.0.0.2:9090/connected-enbs |jq . [ { "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 } ] curl -s http://127.0.0.4:9090/connected-ues |jq . [ { "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" }, { "supi": "imsi-999700000021635", "pdu": [ { "psi": 1, "dnn": "ims", "ipv4": "10.45.0.124", "ipv6": "2001:db8:cafe:79::7a", "snssai": { "sst": 1, "sd": "ffffff" }, "qos_flows": [ { "qfi": 1, "5qi": 5 } ], "pdu_state": "active" } ], "ue_activity": "active" } ]
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
|
||||
* Copyright (C) 2023 by Sukchan Lee <acetcom@gmail.com>
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* 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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
|
||||
* Copyright (C) 2023 by Sukchan Lee <acetcom@gmail.com>
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
@@ -18,21 +19,37 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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 <netdb.h> /* AI_PASSIVE */
|
||||
#include "prom.h"
|
||||
#include "microhttpd.h"
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
247
src/amf/connected_gnbs.c
Normal file
247
src/amf/connected_gnbs.c
Normal file
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
40
src/amf/connected_gnbs.h
Normal file
40
src/amf/connected_gnbs.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* /connected-gnbs — AMF-side JSON exporter (Prometheus HTTP endpoint)
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -61,6 +61,7 @@ libamf_sources = files('''
|
||||
|
||||
init.c
|
||||
metrics.c
|
||||
connected_gnbs.c
|
||||
'''.split())
|
||||
|
||||
libamf = static_library('amf',
|
||||
|
||||
187
src/mme/connected_enbs.c
Normal file
187
src/mme/connected_enbs.c
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
40
src/mme/connected_enbs.h
Normal file
40
src/mme/connected_enbs.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
* /connected-gnbs — AMF-side JSON exporter (Prometheus HTTP endpoint)
|
||||
*
|
||||
* License: AGPLv3+
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#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
|
||||
|
||||
@@ -79,6 +79,7 @@ libmme_sources = files('''
|
||||
mme-path.c
|
||||
sbc-handler.c
|
||||
metrics.c
|
||||
connected_enbs.c
|
||||
'''.split())
|
||||
|
||||
libmme = static_library('mme',
|
||||
|
||||
@@ -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;
|
||||
|
||||
333
src/smf/connected_ues.c
Normal file
333
src/smf/connected_ues.c
Normal file
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 <juraj.elias@gmail.com>
|
||||
* 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 <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
42
src/smf/connected_ues.h
Normal file
42
src/smf/connected_ues.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Minimal public API for connected_ues
|
||||
*/
|
||||
#ifndef SMF_CONNECTED_UES_H
|
||||
#define SMF_CONNECTED_UES_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#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 */
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user