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:
hug0lin
2025-09-13 03:02:01 +02:00
committed by GitHub
parent 606877bf11
commit fc42f3039c
16 changed files with 1137 additions and 104 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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 */

View File

@@ -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
View 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
View 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

View File

@@ -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;

View File

@@ -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
View 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
View 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

View File

@@ -79,6 +79,7 @@ libmme_sources = files('''
mme-path.c
sbc-handler.c
metrics.c
connected_enbs.c
'''.split())
libmme = static_library('mme',

View File

@@ -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
View 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
View 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 */

View File

@@ -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;

View File

@@ -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',