Open5GS JSON API for accessing UE, gNB, eNB, PDU data (#4093)

Open5GS has a lightweight HTTP server (already used for `/metrics`) embedded in each NF.
New optional JSON endpoints were added:

| NF  | Endpoint | Content |
| --- | --- | --- |
| **SMF** | `/pdu-info` | All currently connected UEs + their PDU sessions (IMSI/SUPI, DNN, IPs, S-NSSAI, QoS, state, etc.) |
| **AMF** | `/gnb-info` | All currently connected gNBs and their supported TAs, PLMNs, SCTP info, number of UEs |
| **AMF** | `/ue-info` | All currently connected NR UEs and their info, active gNB, tai, security, slices, am_policy |
| **MME** | `/enb-info` | All currently connected eNBs and their supported TAs, PLMNs, SCTP info, number of UEs |
| **MME** | `/ue-info` | All currently connected LTE UEs and their info, active eNB, tai, pdn info |

They are exposed on the same HTTP port used by Prometheus metrics (default `:9090`).

To reduce processor load when there are a large number of devices, the API includes a pager that limits output.

`/ue-info?page=0&page_size=100`

page in the range 0-n (0 is default),

`page=-1` to avoid paging

`page_size=100` (default and MAX)
This commit is contained in:
Juraj Elias
2025-10-01 14:52:54 +02:00
committed by GitHub
parent cb1f45d2d8
commit 794c63276e
25 changed files with 2437 additions and 841 deletions

View File

@@ -25,26 +25,30 @@
#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;
size_t (*ogs_metrics_pdu_info_dumper)(char *buf, size_t buflen) = NULL;
size_t (*ogs_metrics_ue_info_dumper)(char *buf, size_t buflen) = NULL;
size_t (*ogs_metrics_gnb_info_dumper)(char *buf, size_t buflen) = NULL;
size_t (*ogs_metrics_enb_info_dumper)(char *buf, size_t buflen) = NULL;
void ogs_metrics_register_connected_ues(size_t (*fn)(char *buf, size_t buflen))
void ogs_metrics_register_ue_info(size_t (*fn)(char *buf, size_t buflen))
{
ogs_metrics_connected_ues_dumper = fn;
ogs_metrics_ue_info_dumper = fn;
}
void ogs_metrics_register_connected_gnbs(size_t (*fn)(char *buf, size_t buflen))
void ogs_metrics_register_pdu_info(size_t (*fn)(char *buf, size_t buflen))
{
ogs_metrics_connected_gnbs_dumper = fn;
ogs_metrics_pdu_info_dumper = fn;
}
void ogs_metrics_register_connected_enbs(size_t (*fn)(char *buf, size_t buflen))
void ogs_metrics_register_gnb_info(size_t (*fn)(char *buf, size_t buflen))
{
ogs_metrics_connected_enbs_dumper = fn;
ogs_metrics_gnb_info_dumper = fn;
}
void ogs_metrics_register_enb_info(size_t (*fn)(char *buf, size_t buflen))
{
ogs_metrics_enb_info_dumper = fn;
}
int __ogs_metrics_domain;
static ogs_metrics_context_t self;
static int context_initialized = 0;

View File

@@ -36,17 +36,18 @@
extern "C" {
#endif
/* UEs dumper hook (SMF) */
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 size_t (*ogs_metrics_pdu_info_dumper)(char *buf, size_t buflen);
void ogs_metrics_register_pdu_info(size_t (*fn)(char *buf, size_t buflen));
/* 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));
extern size_t (*ogs_metrics_ue_info_dumper)(char *buf, size_t buflen);
void ogs_metrics_register_ue_info(size_t (*fn)(char *buf, size_t buflen));
extern size_t (*ogs_metrics_gnb_info_dumper)(char *buf, size_t buflen);
void ogs_metrics_register_gnb_info(size_t (*fn)(char *buf, size_t buflen));
extern size_t (*ogs_metrics_enb_info_dumper)(char *buf, size_t buflen);
void ogs_metrics_register_enb_info(size_t (*fn)(char *buf, size_t buflen));
/* eNBs dumper hook (MME) */
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
}

View File

@@ -23,9 +23,10 @@
* 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)
* - /pdu-info (provided by NF registering ogs_metrics_pdu_info_dumper)
* - /gnb-info (provided by NF registering ogs_metrics_gnb_info_dumper)
* - /enb-info (provided by NF registering ogs_metrics_enb_info_dumper)
* - /ue-info (provided by NF registering ogs_metrics_ue_info_dumper)
*/
#include "ogs-core.h"
@@ -35,6 +36,12 @@
#include "prom.h"
#include "microhttpd.h"
#include <string.h>
#include "prometheus/pager.h"
ogs_metrics_pager_fn ogs_metrics_pdu_info_set_pager = NULL;
ogs_metrics_pager_fn ogs_metrics_gnb_info_set_pager = NULL;
ogs_metrics_pager_fn ogs_metrics_enb_info_set_pager = NULL;
ogs_metrics_pager_fn ogs_metrics_ue_info_set_pager = NULL;
extern int __ogs_metrics_domain;
#define MAX_LABELS 8
@@ -75,6 +82,17 @@ static OGS_POOL(metrics_server_pool, ogs_metrics_server_t);
static int ogs_metrics_context_server_start(ogs_metrics_server_t *server);
static int ogs_metrics_context_server_stop(ogs_metrics_server_t *server);
static size_t get_query_size_t(struct MHD_Connection *connection,
const char *key, size_t default_val)
{
const char *val = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, key);
if (!val || !*val) return default_val;
char *end = NULL;
unsigned long long v = strtoull(val, &end, 10);
if (end == val || *end != '\0') return default_val;
return (size_t)v;
}
void ogs_metrics_server_init(ogs_metrics_context_t *ctx)
{
ogs_list_init(&ctx->server_list);
@@ -291,22 +309,45 @@ mhd_server_access_handler(void *cls, struct MHD_Connection *connection,
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 PDUs (SMF) */
if (strcmp(url, "/pdu-info") == 0) {
size_t page = get_query_size_t(connection, "page", 0);
size_t page_size = get_query_size_t(connection, "page_size", 100);
if (ogs_metrics_pdu_info_set_pager)
ogs_metrics_pdu_info_set_pager(page, page_size);
return serve_json_from_dumper(connection, ogs_metrics_pdu_info_dumper,
"pdu-info 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");
if (strcmp(url, "/gnb-info") == 0) {
size_t page = get_query_size_t(connection, "page", 0);
size_t page_size = get_query_size_t(connection, "page_size", 100);
if (ogs_metrics_gnb_info_set_pager)
ogs_metrics_gnb_info_set_pager(page, page_size);
return serve_json_from_dumper(connection, ogs_metrics_gnb_info_dumper,
"gnb-info 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");
if (strcmp(url, "/enb-info") == 0) {
size_t page = get_query_size_t(connection, "page", 0);
size_t page_size = get_query_size_t(connection, "page_size", 100);
if (ogs_metrics_enb_info_set_pager)
ogs_metrics_enb_info_set_pager(page, page_size);
return serve_json_from_dumper(connection, ogs_metrics_enb_info_dumper,
"enb-info endpoint not available on this NF\n");
}
/* JSON: connected UEs (AMF/MME) */
if (strcmp(url, "/ue-info") == 0) {
size_t page = get_query_size_t(connection, "page", 0);
size_t page_size = get_query_size_t(connection, "page_size", 100);
if (ogs_metrics_ue_info_set_pager)
ogs_metrics_ue_info_set_pager(page, page_size);
return serve_json_from_dumper(connection, ogs_metrics_ue_info_dumper,
"ue-info endpoint not available on this NF\n");
}
/* No matching route */

View File

@@ -0,0 +1,121 @@
/*
* 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/>.
*/
/*
* /ue-info — MME-side JSON exporter (Prometheus HTTP endpoint)
*
* License: AGPLv3+
*/
/* lib/metrics/prometheus/json_pager.c */
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include "ogs-core.h"
#include "ogs-proto.h"
#include "sbi/openapi/external/cJSON.h"
#include "metrics/prometheus/json_pager.h"
size_t
json_pager_safe_start_index(bool no_paging, size_t page, size_t page_size)
{
if (no_paging) return 0;
if (page_size != 0 && page > SIZE_MAX / page_size)
return SIZE_MAX; /* treat as beyond end */
return page * page_size;
}
void
json_pager_add_trailing(cJSON *root,
bool no_paging,
size_t page,
size_t page_size,
size_t count,
bool has_next,
const char *base_path,
bool truncated)
{
cJSON *pager = cJSON_CreateObject();
cJSON_AddNumberToObject(pager, "page", (double)(no_paging ? 0 : page));
cJSON_AddNumberToObject(pager, "page_size", (double)(no_paging ? 0 : page_size));
cJSON_AddNumberToObject(pager, "count", (double)count);
/* Only add "truncated" when true */
if (truncated)
cJSON_AddBoolToObject(pager, "truncated", 1);
if (!no_paging) {
if (page > 0) {
char prev[128];
snprintf(prev, sizeof prev, "%s?page=%zu", base_path, page - 1);
cJSON_AddStringToObject(pager, "prev", prev);
}
if (has_next) {
char next[128];
snprintf(next, sizeof next, "%s?page=%zu", base_path, page + 1);
cJSON_AddStringToObject(pager, "next", next);
}
}
cJSON_AddItemToObject(root, "pager", pager);
}
size_t
json_pager_finalize(cJSON *root, char *buf, size_t buflen)
{
size_t outlen = 0;
if (cJSON_PrintPreallocated(root, buf, (int)buflen, 0)) {
outlen = strlen(buf);
cJSON_Delete(root);
return outlen;
}
char *tmp = cJSON_PrintUnformatted(root);
if (!tmp) {
if (buflen >= 3) {
memcpy(buf, "{}", 3);
cJSON_Delete(root);
return 2;
}
if (buflen) buf[0] = '\0';
cJSON_Delete(root);
return 0;
}
size_t need = strlen(tmp);
if (need + 1 <= buflen) {
memcpy(buf, tmp, need + 1);
outlen = need;
} else {
if (buflen >= 3) {
memcpy(buf, "{}", 3);
outlen = 2;
} else if (buflen) {
buf[0] = '\0';
}
}
cJSON_free(tmp);
cJSON_Delete(root);
return outlen;
}

View File

@@ -0,0 +1,69 @@
/*
* 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/>.
*/
/*
* /ue-info — MME-side JSON exporter (Prometheus HTTP endpoint)
*
* License: AGPLv3+
*/
/* lib/metrics/prometheus/json_pager.h */
#ifndef OGS_METRICS_PROM_JSON_PAGER_H
#define OGS_METRICS_PROM_JSON_PAGER_H
#include <stddef.h>
#include <stdbool.h>
/* Forward-declare so callers don't need to include cJSON.h here */
typedef struct cJSON cJSON;
/* Safe start_index = page * page_size with overflow guard. */
size_t json_pager_safe_start_index(bool no_paging, size_t page, size_t page_size);
/* Append trailing { "pager": { page, page_size, count, [truncated], prev?, next? } }. */
void json_pager_add_trailing(cJSON *root,
bool no_paging,
size_t page,
size_t page_size,
size_t count,
bool has_next,
const char *base_path,
bool truncated);
/* Finalize JSON into buf and free root. */
size_t json_pager_finalize(cJSON *root, char *buf, size_t buflen);
/* Paging step helper
* Returns: 1 -> CONTINUE (skip), 2 -> BREAK (page full; set has_next), 0 -> EMIT.
*/
static inline int
json_pager_advance(bool no_paging,
size_t idx, size_t start_index,
size_t emitted, size_t page_size,
bool *has_next)
{
if (no_paging) return 0;
if (idx < start_index) return 1;
if (emitted == page_size) { if (has_next) *has_next = true; return 2; }
return 0;
}
#endif /* OGS_METRICS_PROM_JSON_PAGER_H */

View File

@@ -0,0 +1,38 @@
/*
* 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/>.
*/
#ifndef OGS_METRICS_PROM_PAGER_H
#define OGS_METRICS_PROM_PAGER_H
#include <stddef.h>
/* NF-specific optional pager setter signatures.
* NFs may assign these at init time. If left NULL, paging is ignored for that NF.
*/
typedef void (*ogs_metrics_pager_fn)(size_t page, size_t page_size);
/* Connected PDUs (SMF) */
extern ogs_metrics_pager_fn ogs_metrics_pdu_info_set_pager;
extern ogs_metrics_pager_fn ogs_metrics_ue_info_set_pager;
extern ogs_metrics_pager_fn ogs_metrics_gnb_info_set_pager;
extern ogs_metrics_pager_fn ogs_metrics_enb_info_set_pager;
#endif /* OGS_METRICS_PROM_PAGER_H */

View File

@@ -1,250 +0,0 @@
/*
* 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);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
int n = ogs_vsnprintf(buf + off, buflen - off, fmt, ap);
#pragma GCC diagnostic pop
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;
}

286
src/amf/gnb-info.c Normal file
View File

@@ -0,0 +1,286 @@
/*
* 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/>.
*/
/*
* GNB info - AMF gNBs JSON dumper for the Prometheus HTTP server (/gnb-info).
* - gnb_id, plmn, amf, ng, gnb_IP, supported ta_list, slices, num_connceted_ues
* - pager: /gnb-info?page=0&page_size=100 (0-based, page=-1 without paging) Default: page=0 page_size=100=MAXSIZE
*
* path: http://AMF_IP:9090/gnb-info
* curl -s "http://127.0.0.5:9090/gnb-info?" |jq .
* {
* "items": [
* {
* "gnb_id": 100,
* "plmn": "99970",
* "network": {
* "amf_name": "efire-amf0",
* "ngap_port": 38412
* },
* "ng": {
* "sctp": {
* "peer": "[192.168.168.100]:60110",
* "max_out_streams": 2,
* "next_ostream_id": 1
* },
* "setup_success": true
* },
* "supported_ta_list": [
* {
* "tac": "000001",
* "bplmns": [
* {
* "plmn": "99970",
* "snssai": [
* {
* "sst": 1,
* "sd": "ffffff"
* }
* ]
* },
* {
* "plmn": "99971",
* "snssai": [
* {
* "sst": 2,
* "sd": "000000"
* }
* ]
* }
* ]
* }
* ],
* "num_connected_ues": 2
* }
* ],
* "pager": {
* "page": 0,
* "page_size": 100,
* "count": 1
* }
* }
*/
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <sys/socket.h>
#include "ogs-core.h"
#include "ogs-proto.h"
#include "context.h"
#include "gnb-info.h"
#include "sbi/openapi/external/cJSON.h"
#include "metrics/prometheus/json_pager.h"
#ifndef GNB_INFO_PAGE_SIZE_DEFAULT
#define GNB_INFO_PAGE_SIZE_DEFAULT 100U
#endif
static size_t g_page = SIZE_MAX; /* SIZE_MAX => no paging */
static size_t g_page_size = 0; /* 0 => use default in dumper */
void amf_metrics_gnb_info_set_pager(size_t page, size_t page_size)
{
g_page = page;
g_page_size = page_size;
}
size_t amf_dump_gnb_info(char *buf, size_t buflen)
{
return amf_dump_gnb_info_paged(buf, buflen, g_page, g_page_size);
}
static inline uint32_t u24_to_u32(ogs_uint24_t v)
{
uint32_t x = 0; memcpy(&x, &v, sizeof(v) < sizeof(x) ? sizeof(v) : sizeof(x));
return (x & 0xFFFFFFu);
}
static inline const char *safe_sa_str(ogs_sockaddr_t *sa)
{
if (!sa) return "";
int fam = sa->sa.sa_family;
if (fam != AF_INET && fam != AF_INET6) return "";
return ogs_sockaddr_to_string_static(sa);
}
static inline int add_plmn_string(cJSON *obj, const char *key, const ogs_plmn_id_t *plmn)
{
char s[OGS_PLMNIDSTRLEN] = {0};
if (plmn) ogs_plmn_id_to_string(plmn, s);
return cJSON_AddStringToObject(obj, key, s) ? 0 : -1;
}
/* ---------- main (paged) ---------- */
size_t amf_dump_gnb_info_paged(char *buf, size_t buflen, size_t page, size_t page_size)
{
if (!buf || buflen == 0) return 0;
const bool no_paging = (page == SIZE_MAX);
if (!no_paging) {
if (page_size == 0) page_size = GNB_INFO_PAGE_SIZE_DEFAULT;
if (page_size > GNB_INFO_PAGE_SIZE_DEFAULT) page_size = GNB_INFO_PAGE_SIZE_DEFAULT;
} else {
page_size = SIZE_MAX;
page = 0;
}
amf_context_t *ctxt = amf_self();
cJSON *root = cJSON_CreateObject();
if (!root) { if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; } buf[0] = '\0'; return 0; }
cJSON *items = cJSON_AddArrayToObject(root, "items");
if (!items) { cJSON_Delete(root); if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; } buf[0] = '\0'; return 0; }
size_t idx = 0, emitted = 0;
bool has_next = false, oom = false;
const size_t start_index = json_pager_safe_start_index(no_paging, page, page_size);
amf_gnb_t *gnb = NULL;
ogs_list_for_each(&ctxt->gnb_list, gnb) {
int act = json_pager_advance(no_paging, idx, start_index, emitted, page_size, &has_next);
if (act == 1) { idx++; continue; }
if (act == 2) break;
/* Build the item completely before attaching to items[] */
cJSON *g = cJSON_CreateObject();
if (!g) { oom = true; break; }
/* basics */
if (!cJSON_AddNumberToObject(g, "gnb_id", (double)(unsigned)gnb->gnb_id)) { cJSON_Delete(g); oom = true; break; }
if (add_plmn_string(g, "plmn", &gnb->plmn_id) < 0) { cJSON_Delete(g); oom = true; break; }
/* network */
{
cJSON *network = cJSON_CreateObject();
if (!network) { cJSON_Delete(g); oom = true; break; }
if (!cJSON_AddStringToObject(network, "amf_name", ctxt->amf_name ? ctxt->amf_name : "")) { cJSON_Delete(network); cJSON_Delete(g); oom = true; break; }
if (!cJSON_AddNumberToObject(network, "ngap_port", (double)(unsigned)ctxt->ngap_port)) { cJSON_Delete(network); cJSON_Delete(g); oom = true; break; }
cJSON_AddItemToObjectCS(g, "network", network);
}
/* ng + sctp */
{
cJSON *ng = cJSON_CreateObject();
if (!ng) { cJSON_Delete(g); oom = true; break; }
/* Build sctp first, then attach */
cJSON *sctp = cJSON_CreateObject();
if (!sctp) { cJSON_Delete(ng); cJSON_Delete(g); oom = true; break; }
if (!cJSON_AddStringToObject(sctp, "peer", safe_sa_str(gnb->sctp.addr))) { cJSON_Delete(sctp); cJSON_Delete(ng); cJSON_Delete(g); oom = true; break; }
if (!cJSON_AddNumberToObject(sctp, "max_out_streams", (double)gnb->max_num_of_ostreams)) { cJSON_Delete(sctp); cJSON_Delete(ng); cJSON_Delete(g); oom = true; break; }
if (!cJSON_AddNumberToObject(sctp, "next_ostream_id", (double)(unsigned)gnb->ostream_id)) { cJSON_Delete(sctp); cJSON_Delete(ng); cJSON_Delete(g); oom = true; break; }
cJSON_AddItemToObjectCS(ng, "sctp", sctp);
if (!cJSON_AddBoolToObject(ng, "setup_success", gnb->state.ng_setup_success ? 1 : 0)) { cJSON_Delete(ng); cJSON_Delete(g); oom = true; break; }
cJSON_AddItemToObjectCS(g, "ng", ng);
}
/* supported_ta_list */
{
cJSON *tas = cJSON_CreateArray();
if (!tas) { cJSON_Delete(g); oom = true; break; }
bool inner_oom = false;
for (int t = 0; t < gnb->num_of_supported_ta_list; t++) {
const ogs_uint24_t tac = gnb->supported_ta_list[t].tac;
const int nbp = gnb->supported_ta_list[t].num_of_bplmn_list;
cJSON *ta = cJSON_CreateObject();
if (!ta) { inner_oom = true; break; }
char tac_hex[7]; snprintf(tac_hex, sizeof tac_hex, "%06x", (unsigned)u24_to_u32(tac));
if (!cJSON_AddStringToObject(ta, "tac", tac_hex)) { cJSON_Delete(ta); inner_oom = true; break; }
cJSON *bplmns = cJSON_CreateArray();
if (!bplmns) { cJSON_Delete(ta); inner_oom = true; break; }
bool inner2_oom = false;
for (int p = 0; p < nbp; p++) {
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;
cJSON *bp = cJSON_CreateObject();
if (!bp) { inner2_oom = true; break; }
if (add_plmn_string(bp, "plmn", bp_plmn) < 0) { cJSON_Delete(bp); inner2_oom = true; break; }
cJSON *sns = cJSON_CreateArray();
if (!sns) { cJSON_Delete(bp); inner2_oom = true; break; }
bool inner3_oom = false;
for (int i = 0; i < ns; i++) {
cJSON *o = cJSON_CreateObject();
if (!o) { inner3_oom = true; break; }
if (!cJSON_AddNumberToObject(o, "sst", (double)(unsigned)sn[i].sst)) { cJSON_Delete(o); inner3_oom = true; break; }
char sd[7]; snprintf(sd, sizeof sd, "%06x", (unsigned)u24_to_u32(sn[i].sd));
if (!cJSON_AddStringToObject(o, "sd", sd)) { cJSON_Delete(o); inner3_oom = true; break; }
cJSON_AddItemToArray(sns, o);
}
if (inner3_oom) { cJSON_Delete(sns); cJSON_Delete(bp); inner2_oom = true; break; }
cJSON_AddItemToObjectCS(bp, "snssai", sns);
cJSON_AddItemToArray(bplmns, bp);
}
if (inner2_oom) { cJSON_Delete(bplmns); cJSON_Delete(ta); inner_oom = true; break; }
cJSON_AddItemToObjectCS(ta, "bplmns", bplmns);
cJSON_AddItemToArray(tas, ta);
}
if (inner_oom) { cJSON_Delete(tas); cJSON_Delete(g); oom = true; break; }
cJSON_AddItemToObjectCS(g, "supported_ta_list", tas);
}
/* num_connected_ues */
{
size_t num = 0; ran_ue_t *r = NULL; ogs_list_for_each(&((amf_gnb_t*)gnb)->ran_ue_list, r) num++;
if (!cJSON_AddNumberToObject(g, "num_connected_ues", (double)num)) { cJSON_Delete(g); oom = true; break; }
}
/* attach completed item */
cJSON_AddItemToArray(items, g);
emitted++;
idx++;
}
json_pager_add_trailing(root, no_paging, page, page_size, emitted, has_next && !oom,"/gnb-info", oom);
return json_pager_finalize(root, buf, buflen);
}

43
src/amf/gnb-info.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* 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/>.
*/
/*
* /gnb-info — AMF-side JSON exporter (Prometheus HTTP endpoint)
*
*/
#pragma once
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef GNB_INFO_PAGE_SIZE_DEFAULT
#define GNB_INFO_PAGE_SIZE_DEFAULT 100U
#endif
size_t amf_dump_gnb_info(char *buf, size_t buflen);
size_t amf_dump_gnb_info_paged(char *buf, size_t buflen, size_t page, size_t page_size);
void amf_metrics_gnb_info_set_pager(size_t page, size_t page_size);
#ifdef __cplusplus
}
#endif

View File

@@ -22,7 +22,10 @@
#include "metrics.h"
#include "ogs-metrics.h"
#include "connected_gnbs.h"
#include "metrics/prometheus/json_pager.h"
#include "metrics/prometheus/pager.h"
#include "gnb-info.h"
#include "ue-info.h"
static ogs_thread_t *thread;
static void amf_main(void *data);
@@ -58,7 +61,12 @@ 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);
/* dumpers /gnb-info /ue-info */
ogs_metrics_register_gnb_info(amf_dump_gnb_info);
ogs_metrics_register_ue_info(amf_dump_ue_info);
ogs_metrics_gnb_info_set_pager = amf_metrics_gnb_info_set_pager;
ogs_metrics_ue_info_set_pager = amf_metrics_ue_info_set_pager;
rv = amf_sbi_open();
if (rv != OGS_OK) return rv;

View File

@@ -61,7 +61,9 @@ libamf_sources = files('''
init.c
metrics.c
connected_gnbs.c
gnb-info.c
ue-info.c
../../lib/metrics/prometheus/json_pager.c
'''.split())
libamf = static_library('amf',

633
src/amf/ue-info.c Normal file
View File

@@ -0,0 +1,633 @@
/*
* 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 AMF UEs JSON dumper for the Prometheus HTTP server (/ue-info).
* - supi, suci, pei, cm_state, guti,m_tmsi, gnb_info, location, security, ambr, slices, am_policy
* - pager: /ue-info?page=0&page_size=100 (0-based, page=-1 without paging) Default: page=0 page_size=100=MAXSIZE
*
* path: http://AMF_IP:9090/ue-info
* curl -s "http://127.0.0.5:9090/ue-info" |jq .
* {
* "items": [
* {
* "supi": "imsi-001010000056492",
* "suci": "suci-0-001-01-0000-1-1-85731c4241d466407311a85135e914987944f361550f13f4532e29bdc5859548a363cc1a81798bb4cc59f2b4db",
* "pei": "imeisv-3535938300494715",
* "cm_state": "connected",
* "guti": "00101-030080-CC00007AB",
* "m_tmsi": 3221227435,
* "gnb": {
* "ostream_id": 1,
* "amf_ue_ngap_id": 2,
* "ran_ue_ngap_id": 39,
* "gnb_id": 100,
* "cell_id": 1
* },
* "location": {
* "timestamp": 1758733375895512,
* "nr_tai": {
* "plmn": "99970",
* "tac_hex": "000001",
* "tac": 1
* },
* "nr_cgi": {
* "plmn": "99970",
* "nci": 1638401,
* "gnb_id": 100,
* "cell_id": 1
* },
* "last_visited_plmn_id": "000000"
* },
* "msisdn": [],
* "security": {
* "valid": 1,
* "enc": "nea2",
* "int": "nia2"
* },
* "ambr": {
* "downlink": 1000000000,
* "uplink": 1000000000
* },
* "pdu_sessions": [],
* "pdu_sessions_count": 0,
* "requested_slices": [
* {
* "sst": 1,
* "sd": "ffffff"
* }
* ],
* "allowed_slices": [
* {
* "sst": 1,
* "sd": "ffffff"
* }
* ],
* "requested_slices_count": 1,
* "allowed_slices_count": 1,
* "am_policy_features": 4,
* "am_policy_features_info": {
* "hex": "0x0000000000000004",
* "bits": [
* 2
* ],
* "labels": [
* "QoS Policy Control"
* ]
* }
* }
* ],
* "pager": {
* "page": 0,
* "page_size": 100,
* "count": 2
* }
* }
*/
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <inttypes.h>
#include <string.h>
#include "ogs-core.h"
#include "ogs-proto.h"
#include "context.h"
#include "ue-info.h"
#include "sbi/openapi/external/cJSON.h"
#include "metrics/prometheus/json_pager.h"
static size_t g_ue_page = 0;
static size_t g_ue_page_size = 0;
void amf_metrics_ue_info_set_pager(size_t page, size_t page_size)
{
g_ue_page = page;
g_ue_page_size = page_size;
}
size_t amf_dump_ue_info(char *buf, size_t buflen)
{
size_t page = g_ue_page;
size_t page_size = g_ue_page_size ? g_ue_page_size : 100;
if (page_size > 100) page_size = 100;
return amf_dump_ue_info_paged(buf, buflen, page, page_size);
}
static inline uint32_t u24_to_u32(ogs_uint24_t v)
{
uint32_t x = 0;
memcpy(&x, &v, sizeof(v) < sizeof(x) ? sizeof(v) : sizeof(x));
return (x & 0xFFFFFFu);
}
static inline void bytes3_to_hex_lower(const void *p, char out[7])
{
const uint8_t *b = (const uint8_t *)p;
static const char *hx = "0123456789abcdef";
out[0] = hx[(b[0] >> 4) & 0xF];
out[1] = hx[b[0] & 0xF];
out[2] = hx[(b[1] >> 4) & 0xF];
out[3] = hx[b[1] & 0xF];
out[4] = hx[(b[2] >> 4) & 0xF];
out[5] = hx[b[2] & 0xF];
out[6] = '\0';
}
/* AM policy feature labels */
static const char *am_policy_feature_names[64] = {
/*0*/ "AM Policy Association",
/*1*/ "Slice-specific Policy",
/*2*/ "QoS Policy Control",
/*3*/ "Access & Mobility Policy",
/*4*/ "Charging Policy",
/*5*/ "Traffic Steering",
/*6*/ "Roaming Policy",
/*7*/ "Emergency Services Policy",
/*8*/ "UE Policy",
/*9*/ "Session Continuity Policy",
/*10*/ "Application-based Policy",
/*11*/ "Location-based Policy",
/*12*/ "Time-based Policy",
/*13*/ "Data Network Selection",
/*14*/ "Policy Update Notification",
/*15*/ "Policy Control Event Exposure",
};
/* -------- JSON builders: 0=OK, -1=OOM. Children are attached only when complete. -------- */
static int add_snssai_sd_string(cJSON *obj, const char *key, const void *sd_ptr)
{
char sd_hex[7] = "ffffff";
if (sd_ptr) bytes3_to_hex_lower(sd_ptr, sd_hex);
cJSON *s = cJSON_CreateString(sd_hex);
if (!s) return -1;
cJSON_AddItemToObjectCS(obj, key, s);
return 0;
}
static int add_basic_identity(cJSON *o, const amf_ue_t *ue)
{
if (ue->supi && ue->supi[0]) {
cJSON *v = cJSON_CreateString(ue->supi); if (!v) return -1;
cJSON_AddItemToObjectCS(o, "supi", v);
}
if (ue->suci && ue->suci[0]) {
cJSON *v = cJSON_CreateString(ue->suci); if (!v) return -1;
cJSON_AddItemToObjectCS(o, "suci", v);
}
if (ue->pei && ue->pei[0]) {
cJSON *v = cJSON_CreateString(ue->pei); if (!v) return -1;
cJSON_AddItemToObjectCS(o, "pei", v);
}
{
const char *cm = CM_CONNECTED(ue) ? "connected" : "idle";
cJSON *v = cJSON_CreateString(cm); if (!v) return -1;
cJSON_AddItemToObjectCS(o, "cm_state", v);
}
/* If M-TMSI present, expose GUTI string and m_tmsi numeric */
if (ue->current.m_tmsi) {
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
ogs_plmn_id_to_string(&ue->guami->plmn_id, plmn_str);
/* ogs_amf_id_to_string() returns heap memory — free it */
char *amf_hex = ogs_amf_id_to_string(&ue->guami->amf_id);
const char *amf_str = amf_hex ? amf_hex : "";
char guti_buf[80];
(void)snprintf(guti_buf, sizeof guti_buf, "%s-%s-C%08X",
plmn_str, amf_str, (unsigned)ue->current.guti.m_tmsi);
cJSON *g = cJSON_CreateString(guti_buf);
if (amf_hex) ogs_free(amf_hex);
if (!g) return -1;
cJSON_AddItemToObjectCS(o, "guti", g);
cJSON *m = cJSON_CreateNumber((double)ue->current.guti.m_tmsi);
if (!m) return -1;
cJSON_AddItemToObjectCS(o, "m_tmsi", m);
}
return 0;
}
static int add_gnb(cJSON *parent, const amf_ue_t *ue)
{
cJSON *gnb = cJSON_CreateObject();
if (!gnb) return -1;
cJSON *osid = cJSON_CreateNumber((double)ue->gnb_ostream_id);
if (!osid) { cJSON_Delete(gnb); return -1; }
cJSON_AddItemToObjectCS(gnb, "ostream_id", osid);
ran_ue_t *ran = ran_ue_find_by_id(ue->ran_ue_id);
if (ran) {
uint64_t nci = ue->nr_cgi.cell_id & 0xFFFFFFFFFULL; /* 36-bit */
uint32_t gnb_id = (uint32_t)((nci >> 14) & 0x3FFFFF);
uint32_t cell_id = (uint32_t)(nci & 0x3FFF);
cJSON *a = cJSON_CreateNumber((double)ran->amf_ue_ngap_id);
cJSON *r = cJSON_CreateNumber((double)ran->ran_ue_ngap_id);
cJSON *g = cJSON_CreateNumber((double)gnb_id);
cJSON *c = cJSON_CreateNumber((double)cell_id);
if (!a || !r || !g || !c) {
if (a) cJSON_Delete(a);
if (r) cJSON_Delete(r);
if (g) cJSON_Delete(g);
if (c) cJSON_Delete(c);
cJSON_Delete(gnb);
return -1;
}
cJSON_AddItemToObjectCS(gnb, "amf_ue_ngap_id", a);
cJSON_AddItemToObjectCS(gnb, "ran_ue_ngap_id", r);
cJSON_AddItemToObjectCS(gnb, "gnb_id", g);
cJSON_AddItemToObjectCS(gnb, "cell_id", c);
} else {
cJSON *st = cJSON_CreateString("not-connected");
if (!st) { cJSON_Delete(gnb); return -1; }
cJSON_AddItemToObjectCS(gnb, "status", st);
}
cJSON_AddItemToObjectCS(parent, "gnb", gnb);
return 0;
}
static int add_location(cJSON *parent, const amf_ue_t *ue)
{
cJSON *loc = cJSON_CreateObject();
if (!loc) return -1;
cJSON *ts = cJSON_CreateNumber((double)ue->ue_location_timestamp);
if (!ts) { cJSON_Delete(loc); return -1; }
cJSON_AddItemToObjectCS(loc, "timestamp", ts);
/* nr_tai */
{
cJSON *tai = cJSON_CreateObject();
if (!tai) { cJSON_Delete(loc); return -1; }
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
ogs_plmn_id_to_string(&ue->nr_tai.plmn_id, plmn_str);
cJSON *p = cJSON_CreateString(plmn_str);
if (!p) { cJSON_Delete(tai); cJSON_Delete(loc); return -1; }
cJSON_AddItemToObjectCS(tai, "plmn", p);
char tac_hex[7];
(void)snprintf(tac_hex, sizeof tac_hex, "%06x", (unsigned)u24_to_u32(ue->nr_tai.tac));
cJSON *th = cJSON_CreateString(tac_hex);
cJSON *tn = cJSON_CreateNumber((double)u24_to_u32(ue->nr_tai.tac));
if (!th || !tn) { if (th) cJSON_Delete(th); if (tn) cJSON_Delete(tn); cJSON_Delete(tai); cJSON_Delete(loc); return -1; }
cJSON_AddItemToObjectCS(tai, "tac_hex", th);
cJSON_AddItemToObjectCS(tai, "tac", tn);
cJSON_AddItemToObjectCS(loc, "nr_tai", tai);
}
/* nr_cgi */
{
cJSON *cgi = cJSON_CreateObject();
if (!cgi) { cJSON_Delete(loc); return -1; }
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
ogs_plmn_id_to_string(&ue->nr_cgi.plmn_id, plmn_str);
cJSON *p = cJSON_CreateString(plmn_str);
if (!p) { cJSON_Delete(cgi); cJSON_Delete(loc); return -1; }
cJSON_AddItemToObjectCS(cgi, "plmn", p);
uint64_t nci = ue->nr_cgi.cell_id & 0xFFFFFFFFFULL; /* 36-bit */
uint32_t gnb_id = (uint32_t)((nci >> 14) & 0x3FFFFF);
uint32_t cell_id = (uint32_t)(nci & 0x3FFF);
cJSON *n = cJSON_CreateNumber((double)nci);
cJSON *g = cJSON_CreateNumber((double)gnb_id);
cJSON *c = cJSON_CreateNumber((double)cell_id);
if (!n || !g || !c) { if (n) cJSON_Delete(n); if (g) cJSON_Delete(g); if (c) cJSON_Delete(c); cJSON_Delete(cgi); cJSON_Delete(loc); return -1; }
cJSON_AddItemToObjectCS(cgi, "nci", n);
cJSON_AddItemToObjectCS(cgi, "gnb_id", g);
cJSON_AddItemToObjectCS(cgi, "cell_id", c);
cJSON_AddItemToObjectCS(loc, "nr_cgi", cgi);
}
/* last_visited_plmn_id */
{
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
ogs_plmn_id_to_string(&ue->last_visited_plmn_id, plmn_str);
cJSON *lv = cJSON_CreateString(plmn_str);
if (!lv) { cJSON_Delete(loc); return -1; }
cJSON_AddItemToObjectCS(loc, "last_visited_plmn_id", lv);
}
cJSON_AddItemToObjectCS(parent, "location", loc);
return 0;
}
static int add_msisdn_array(cJSON *parent, const amf_ue_t *ue)
{
cJSON *arr = cJSON_CreateArray();
if (!arr) return -1;
for (int i = 0; i < ue->num_of_msisdn; i++) {
if (!ue->msisdn[i] || !ue->msisdn[i][0]) continue;
cJSON *s = cJSON_CreateString(ue->msisdn[i]);
if (!s) { cJSON_Delete(arr); return -1; }
cJSON_AddItemToArray(arr, s);
}
cJSON_AddItemToObjectCS(parent, "msisdn", arr);
return 0;
}
static int add_security(cJSON *parent, const amf_ue_t *ue)
{
cJSON *sec = cJSON_CreateObject();
if (!sec) return -1;
{
int valid_ctx = SECURITY_CONTEXT_IS_VALID(ue);
cJSON *v = cJSON_CreateNumber((double)valid_ctx);
if (!v) { cJSON_Delete(sec); return -1; }
cJSON_AddItemToObjectCS(sec, "valid", v);
}
const char *enc =
ue->selected_enc_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NEA2 ? "nea2" :
ue->selected_enc_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NEA1 ? "nea1" :
ue->selected_enc_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NEA3 ? "nea3" : "nea0";
const char *integ =
ue->selected_int_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NIA2 ? "nia2" :
ue->selected_int_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NIA1 ? "nia1" :
ue->selected_int_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NIA3 ? "nia3" : "nia0";
cJSON *e = cJSON_CreateString(enc);
cJSON *i = cJSON_CreateString(integ);
if (!e || !i) { if (e) cJSON_Delete(e); if (i) cJSON_Delete(i); cJSON_Delete(sec); return -1; }
cJSON_AddItemToObjectCS(sec, "enc", e);
cJSON_AddItemToObjectCS(sec, "int", i);
cJSON_AddItemToObjectCS(parent, "security", sec);
return 0;
}
static int add_ambr(cJSON *parent, const amf_ue_t *ue)
{
cJSON *ambr = cJSON_CreateObject();
if (!ambr) return -1;
cJSON *dl = cJSON_CreateNumber((double)ue->ue_ambr.downlink);
cJSON *ul = cJSON_CreateNumber((double)ue->ue_ambr.uplink);
if (!dl || !ul) { if (dl) cJSON_Delete(dl); if (ul) cJSON_Delete(ul); cJSON_Delete(ambr); return -1; }
cJSON_AddItemToObjectCS(ambr, "downlink", dl);
cJSON_AddItemToObjectCS(ambr, "uplink", ul);
cJSON_AddItemToObjectCS(parent, "ambr", ambr);
return 0;
}
static int add_pdu_sessions(cJSON *parent, const amf_ue_t *ue)
{
cJSON *arr = cJSON_CreateArray();
if (!arr) return -1;
int count = 0;
amf_sess_t *sess = NULL;
ogs_list_for_each(&(ue->sess_list), sess) {
cJSON *it = cJSON_CreateObject();
if (!it) { cJSON_Delete(arr); return -1; }
/* PSI */
{
cJSON *n = cJSON_CreateNumber((double)sess->psi);
if (!n) { cJSON_Delete(it); cJSON_Delete(arr); return -1; }
cJSON_AddItemToObjectCS(it, "psi", n);
}
/* DNN */
if (sess->dnn && sess->dnn[0]) {
cJSON *d = cJSON_CreateString(sess->dnn);
if (!d) { cJSON_Delete(it); cJSON_Delete(arr); return -1; }
cJSON_AddItemToObjectCS(it, "dnn", d);
}
/* S-NSSAI */
{
cJSON *sn = cJSON_CreateObject();
if (!sn) { cJSON_Delete(it); cJSON_Delete(arr); return -1; }
cJSON *sst = cJSON_CreateNumber((double)sess->s_nssai.sst);
if (!sst) { cJSON_Delete(sn); cJSON_Delete(it); cJSON_Delete(arr); return -1; }
cJSON_AddItemToObjectCS(sn, "sst", sst);
if (add_snssai_sd_string(sn, "sd", &sess->s_nssai.sd) < 0) { cJSON_Delete(sn); cJSON_Delete(it); cJSON_Delete(arr); return -1; }
cJSON_AddItemToObjectCS(it, "snssai", sn);
}
/* Flags/counters mirrored as-is */
{
cJSON *lbo = cJSON_CreateBool(sess->lbo_roaming_allowed);
cJSON *rs = cJSON_CreateNumber((double)sess->resource_status);
cJSON *n1 = cJSON_CreateBool(sess->n1_released);
cJSON *n2 = cJSON_CreateBool(sess->n2_released);
if (!lbo || !rs || !n1 || !n2) {
if (lbo) cJSON_Delete(lbo);
if (rs) cJSON_Delete(rs);
if (n1) cJSON_Delete(n1);
if (n2) cJSON_Delete(n2);
cJSON_Delete(it);
cJSON_Delete(arr);
return -1;
}
cJSON_AddItemToObjectCS(it, "lbo_roaming_allowed", lbo);
cJSON_AddItemToObjectCS(it, "resource_status", rs);
cJSON_AddItemToObjectCS(it, "n1_released", n1);
cJSON_AddItemToObjectCS(it, "n2_released", n2);
}
cJSON_AddItemToArray(arr, it);
count++;
}
cJSON_AddItemToObjectCS(parent, "pdu_sessions", arr);
{
cJSON *n = cJSON_CreateNumber((double)count);
if (!n) return -1;
cJSON_AddItemToObjectCS(parent, "pdu_sessions_count", n);
}
return 0;
}
static int add_requested_allowed_slices(cJSON *parent, const amf_ue_t *ue)
{
cJSON *req = cJSON_CreateArray();
cJSON *allow = cJSON_CreateArray();
if (!req || !allow) { if (req) cJSON_Delete(req); if (allow) cJSON_Delete(allow); return -1; }
/* requested */
for (int i = 0; i < ue->requested_nssai.num_of_s_nssai; i++) {
const ogs_nas_s_nssai_ie_t *ie = &ue->requested_nssai.s_nssai[i];
cJSON *sn = cJSON_CreateObject();
if (!sn) { cJSON_Delete(req); cJSON_Delete(allow); return -1; }
cJSON *sst = cJSON_CreateNumber((double)ie->sst);
if (!sst) { cJSON_Delete(sn); cJSON_Delete(req); cJSON_Delete(allow); return -1; }
cJSON_AddItemToObjectCS(sn, "sst", sst);
if (add_snssai_sd_string(sn, "sd", &ie->sd) < 0) { cJSON_Delete(sn); cJSON_Delete(req); cJSON_Delete(allow); return -1; }
cJSON_AddItemToArray(req, sn);
}
/* allowed */
for (int i = 0; i < ue->allowed_nssai.num_of_s_nssai; i++) {
const ogs_nas_s_nssai_ie_t *ie = &ue->allowed_nssai.s_nssai[i];
cJSON *sn = cJSON_CreateObject();
if (!sn) { cJSON_Delete(req); cJSON_Delete(allow); return -1; }
cJSON *sst = cJSON_CreateNumber((double)ie->sst);
if (!sst) { cJSON_Delete(sn); cJSON_Delete(req); cJSON_Delete(allow); return -1; }
cJSON_AddItemToObjectCS(sn, "sst", sst);
if (add_snssai_sd_string(sn, "sd", &ie->sd) < 0) { cJSON_Delete(sn); cJSON_Delete(req); cJSON_Delete(allow); return -1; }
cJSON_AddItemToArray(allow, sn);
}
cJSON_AddItemToObjectCS(parent, "requested_slices", req);
cJSON_AddItemToObjectCS(parent, "allowed_slices", allow);
cJSON *rc = cJSON_CreateNumber((double)ue->requested_nssai.num_of_s_nssai);
cJSON *ac = cJSON_CreateNumber((double)ue->allowed_nssai.num_of_s_nssai);
if (!rc || !ac) { if (rc) cJSON_Delete(rc); if (ac) cJSON_Delete(ac); return -1; }
cJSON_AddItemToObjectCS(parent, "requested_slices_count", rc);
cJSON_AddItemToObjectCS(parent, "allowed_slices_count", ac);
return 0;
}
static int add_am_policy_features(cJSON *parent, const amf_ue_t *ue)
{
uint64_t f = ue->am_policy_control_features;
cJSON *fv = cJSON_CreateNumber((double)f);
if (!fv) return -1;
cJSON_AddItemToObjectCS(parent, "am_policy_features", fv);
cJSON *feat = cJSON_CreateObject();
if (!feat) return -1;
char hex[2 + 16 + 1];
(void)snprintf(hex, sizeof hex, "0x%016" PRIx64, f);
cJSON *hx = cJSON_CreateString(hex);
if (!hx) { cJSON_Delete(feat); return -1; }
cJSON_AddItemToObjectCS(feat, "hex", hx);
cJSON *bits = cJSON_CreateArray();
cJSON *labels = cJSON_CreateArray();
if (!bits || !labels) { if (bits) cJSON_Delete(bits); if (labels) cJSON_Delete(labels); cJSON_Delete(feat); return -1; }
for (int i = 0; i < 64; i++) {
if ((f >> i) & 1ULL) {
cJSON *bi = cJSON_CreateNumber((double)i);
if (!bi) { cJSON_Delete(bits); cJSON_Delete(labels); cJSON_Delete(feat); return -1; }
cJSON_AddItemToArray(bits, bi);
const char *label = (i < 64 && am_policy_feature_names[i] && am_policy_feature_names[i][0])
? am_policy_feature_names[i] : NULL;
char fb[16];
if (!label) { (void)snprintf(fb, sizeof fb, "bit%d", i); label = fb; }
cJSON *ls = cJSON_CreateString(label);
if (!ls) { cJSON_Delete(bits); cJSON_Delete(labels); cJSON_Delete(feat); return -1; }
cJSON_AddItemToArray(labels, ls);
}
}
cJSON_AddItemToObjectCS(feat, "bits", bits);
cJSON_AddItemToObjectCS(feat, "labels", labels);
cJSON_AddItemToObjectCS(parent, "am_policy_features_info", feat);
return 0;
}
static cJSON *amf_ue_to_json(const amf_ue_t *ue)
{
cJSON *o = cJSON_CreateObject();
if (!o) return NULL;
if (add_basic_identity(o, ue) < 0) goto fail;
if (add_gnb(o, ue) < 0) goto fail;
if (add_location(o, ue) < 0) goto fail;
if (add_msisdn_array(o, ue) < 0) goto fail;
if (add_security(o, ue) < 0) goto fail;
if (add_ambr(o, ue) < 0) goto fail;
if (add_pdu_sessions(o, ue) < 0) goto fail;
if (add_requested_allowed_slices(o, ue) < 0) goto fail;
if (add_am_policy_features(o, ue) < 0) goto fail;
return o;
fail:
cJSON_Delete(o);
return NULL;
}
size_t amf_dump_ue_info_paged(char *buf, size_t buflen, size_t page, size_t page_size)
{
if (!buf || buflen == 0) return 0;
const bool no_paging = (page == SIZE_MAX);
if (!no_paging) {
if (page_size == 0) page_size = 100;
if (page_size > 100) page_size = 100;
} else {
page_size = SIZE_MAX;
page = 0;
}
const size_t start_index = json_pager_safe_start_index(no_paging, page, page_size);
amf_context_t *ctxt = amf_self();
cJSON *root = cJSON_CreateObject();
if (!root) { if (buflen) buf[0] = '\0'; return 0; }
cJSON *items = cJSON_CreateArray();
if (!items) { cJSON_Delete(root); if (buflen) buf[0] = '\0'; return 0; }
cJSON_AddItemToObjectCS(root, "items", items);
size_t idx = 0, emitted = 0;
bool has_next = false;
bool oom = false;
amf_ue_t *ue = NULL;
ogs_list_for_each(&ctxt->amf_ue_list, ue) {
int act = json_pager_advance(no_paging, idx, start_index, emitted, page_size, &has_next);
if (act == 1) { idx++; continue; }
if (act == 2) break;
cJSON *one = amf_ue_to_json(ue);
if (!one) { oom = true; break; }
cJSON_AddItemToArray(items, one);
emitted++;
idx++;
}
/* add trailing pager info (json_pager_finalize will free 'root') */
json_pager_add_trailing(root, no_paging, page, page_size, emitted,
has_next && !oom, "/ue-info", oom);
return json_pager_finalize(root, buf, buflen);
}

View File

@@ -18,7 +18,7 @@
*/
/*
* /connected-gnbs AMF-side JSON exporter (Prometheus HTTP endpoint)
* /ue-info AMF-side JSON exporter (Prometheus HTTP endpoint)
*
*/
#pragma once
@@ -29,10 +29,13 @@
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);
#ifndef UE_INFO_PAGE_SIZE_DEFAULT
#define UE_INFO_PAGE_SIZE_DEFAULT 100U
#endif
size_t amf_dump_ue_info(char *buf, size_t buflen);
size_t amf_dump_ue_info_paged(char *buf, size_t buflen, size_t page, size_t page_size);
void amf_metrics_ue_info_set_pager(size_t page, size_t page_size);
#ifdef __cplusplus
}

View File

@@ -1,190 +0,0 @@
/*
* 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);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
int n = ogs_vsnprintf(buf + off, buflen - off, fmt, ap);
#pragma GCC diagnostic pop
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;
}

250
src/mme/enb-info.c Normal file
View File

@@ -0,0 +1,250 @@
/*
* 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/>.
*/
/*
* ENB info - MME eNBs JSON dumper for the Prometheus HTTP server (/enb-info).
* - enb_id, plmn, s1 info, enb_IP, supported ta_list, num_connceted_ues
* - pager: /enb-info?page=0&page_size=100 (0-based, page=-1 without paging) Default: page=0 page_size=100=MAXSIZE
*
* curl -s "http://127.0.0.2:9090/enb-info?" |jq .
* {
* "items": [
* {
* "enb_id": 264040,
* "plmn": "99970",
* "network": {
* "mme_name": "efire-mme0"
* },
* "s1": {
* "sctp": {
* "peer": "[192.168.168.254]:36412",
* "max_out_streams": 10,
* "next_ostream_id": 3
* },
* "setup_success": true
* },
* "supported_ta_list": [
* {
* "tac": "0001",
* "plmn": "99970"
* }
* ],
* "num_connected_ues": 1
* }
* ],
* "pager": {
* "page": 0,
* "page_size": 100,
* "count": 1
* }
* }
*/
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <sys/socket.h>
#include "ogs-core.h"
#include "ogs-proto.h"
#include "mme-context.h"
#include "enb-info.h"
#include "sbi/openapi/external/cJSON.h"
#include "metrics/prometheus/json_pager.h"
#ifndef ENB_INFO_PAGE_SIZE_DEFAULT
#define ENB_INFO_PAGE_SIZE_DEFAULT 100U
#endif
static size_t g_enb_page = 0;
static size_t g_enb_page_size = 0;
void mme_metrics_enb_info_set_pager(size_t page, size_t page_size)
{
g_enb_page = page;
g_enb_page_size = page_size;
}
size_t mme_dump_enb_info(char *buf, size_t buflen)
{
size_t page = g_enb_page;
size_t page_size = g_enb_page_size ? g_enb_page_size : ENB_INFO_PAGE_SIZE_DEFAULT;
if (page_size > ENB_INFO_PAGE_SIZE_DEFAULT) page_size = ENB_INFO_PAGE_SIZE_DEFAULT;
return mme_dump_enb_info_paged(buf, buflen, page, page_size);
}
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);
}
size_t mme_dump_enb_info_paged(char *buf, size_t buflen, size_t page, size_t page_size)
{
if (!buf || buflen == 0) return 0;
const bool no_paging = (page == SIZE_MAX);
if (!no_paging) {
if (page_size == 0) page_size = ENB_INFO_PAGE_SIZE_DEFAULT;
if (page_size > ENB_INFO_PAGE_SIZE_DEFAULT) page_size = ENB_INFO_PAGE_SIZE_DEFAULT;
} else {
page_size = SIZE_MAX;
page = 0;
}
const size_t start_index = json_pager_safe_start_index(no_paging, page, page_size);
mme_context_t *ctxt = mme_self();
/* root */
cJSON *root = cJSON_CreateObject();
if (!root) {
if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; }
if (buflen) buf[0] = '\0';
return 0;
}
/* items array */
cJSON *items = cJSON_AddArrayToObject(root, "items");
if (!items) {
cJSON_Delete(root);
if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; }
if (buflen) buf[0] = '\0';
return 0;
}
size_t idx = 0, emitted = 0;
bool has_next = false;
bool oom = false;
mme_enb_t *enb = NULL;
ogs_list_for_each(&ctxt->enb_list, enb) {
int act = json_pager_advance(no_paging, idx, start_index, emitted, page_size, &has_next);
if (act == 1) { idx++; continue; }
if (act == 2) break;
/* Count connected UEs on this eNB */
size_t num_connected_ues = 0;
{
enb_ue_t *ue_it = NULL;
ogs_list_for_each(&enb->enb_ue_list, ue_it) num_connected_ues++;
}
/* eNB object (build fully before attaching) */
cJSON *e = cJSON_CreateObject();
if (!e) { oom = true; break; }
/* enb_id */
if (!cJSON_AddNumberToObject(e, "enb_id", (double)(unsigned)enb->enb_id)) { cJSON_Delete(e); oom = true; break; }
/* plmn */
{
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
ogs_plmn_id_to_string(&enb->plmn_id, plmn_str);
if (!cJSON_AddStringToObject(e, "plmn", plmn_str)) { cJSON_Delete(e); oom = true; break; }
}
/* network */
{
cJSON *network = cJSON_CreateObject();
if (!network) { cJSON_Delete(e); oom = true; break; }
if (!cJSON_AddStringToObject(network, "mme_name", ctxt->mme_name ? ctxt->mme_name : "")) {
cJSON_Delete(network); cJSON_Delete(e); oom = true; break;
}
cJSON_AddItemToObjectCS(e, "network", network);
}
/* s1 + sctp block */
{
cJSON *s1 = cJSON_CreateObject();
if (!s1) { cJSON_Delete(e); oom = true; break; }
if (!cJSON_AddBoolToObject(s1, "setup_success", enb->state.s1_setup_success ? 1 : 0)) {
cJSON_Delete(s1); cJSON_Delete(e); oom = true; break;
}
/* sctp */
cJSON *sctp = cJSON_CreateObject();
if (!sctp) { cJSON_Delete(s1); cJSON_Delete(e); oom = true; break; }
if (!cJSON_AddStringToObject(sctp, "peer", safe_sa_str(enb->sctp.addr))) {
cJSON_Delete(sctp); cJSON_Delete(s1); cJSON_Delete(e); oom = true; break;
}
if (!cJSON_AddNumberToObject(sctp, "max_out_streams", (double)enb->max_num_of_ostreams)) {
cJSON_Delete(sctp); cJSON_Delete(s1); cJSON_Delete(e); oom = true; break;
}
if (!cJSON_AddNumberToObject(sctp, "next_ostream_id", (double)(unsigned)enb->ostream_id)) {
cJSON_Delete(sctp); cJSON_Delete(s1); cJSON_Delete(e); oom = true; break;
}
cJSON_AddItemToObjectCS(s1, "sctp", sctp);
cJSON_AddItemToObjectCS(e, "s1", s1);
}
/* supported_ta_list (LTE TAC is 16-bit) */
{
cJSON *tas = cJSON_CreateArray();
if (!tas) { cJSON_Delete(e); oom = true; break; }
bool inner_oom = false;
for (int t = 0; t < enb->num_of_supported_ta_list; t++) {
cJSON *ta = cJSON_CreateObject();
if (!ta) { inner_oom = true; break; }
char tac_hex[5];
snprintf(tac_hex, sizeof tac_hex, "%04X", (unsigned)enb->supported_ta_list[t].tac);
if (!cJSON_AddStringToObject(ta, "tac", tac_hex)) {
cJSON_Delete(ta); inner_oom = true; break;
}
char ta_plmn[OGS_PLMNIDSTRLEN] = {0};
ogs_plmn_id_to_string(&enb->supported_ta_list[t].plmn_id, ta_plmn);
if (!cJSON_AddStringToObject(ta, "plmn", ta_plmn)) {
cJSON_Delete(ta); inner_oom = true; break;
}
cJSON_AddItemToArray(tas, ta);
}
if (inner_oom) { cJSON_Delete(tas); cJSON_Delete(e); oom = true; break; }
cJSON_AddItemToObjectCS(e, "supported_ta_list", tas);
}
/* num_connected_ues */
if (!cJSON_AddNumberToObject(e, "num_connected_ues", (double)num_connected_ues)) {
cJSON_Delete(e); oom = true; break;
}
/* success -> append to items[] */
cJSON_AddItemToArray(items, e);
emitted++;
idx++;
}
json_pager_add_trailing(root, no_paging, page, page_size,
emitted, has_next && !oom, "/enb-info", oom);
return json_pager_finalize(root, buf, buflen);
}

43
src/mme/enb-info.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* 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/>.
*/
/*
* /enb-info — MME-side JSON exporter (Prometheus HTTP endpoint)
*
* License: AGPLv3+
*/
#pragma once
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef ENB_INFO_PAGE_SIZE_DEFAULT
#define ENB_INFO_PAGE_SIZE_DEFAULT 100U
#endif
size_t mme_dump_enb_info(char *buf, size_t buflen);
size_t mme_dump_enb_info_paged(char *buf, size_t buflen, size_t page, size_t page_size);
void mme_metrics_enb_info_set_pager(size_t page, size_t page_size);
#ifdef __cplusplus
}
#endif

View File

@@ -79,7 +79,9 @@ libmme_sources = files('''
mme-path.c
sbc-handler.c
metrics.c
connected_enbs.c
enb-info.c
ue-info.c
../../lib/metrics/prometheus/json_pager.c
'''.split())
libmme = static_library('mme',
@@ -89,7 +91,8 @@ libmme = static_library('mme',
libs1ap_dep,
libnas_eps_dep,
libdiameter_s6a_dep,
libgtp_dep],
libgtp_dep,
libsbi_dep],
install : false)
libmme_dep = declare_dependency(
@@ -99,6 +102,7 @@ libmme_dep = declare_dependency(
libs1ap_dep,
libnas_eps_dep,
libdiameter_s6a_dep,
libsbi_dep,
libgtp_dep])
mme_sources = files('''

View File

@@ -30,7 +30,10 @@
#include "sgsap-path.h"
#include "mme-gtp-path.h"
#include "metrics.h"
#include "connected_enbs.h"
#include "metrics/prometheus/json_pager.h"
#include "metrics/prometheus/pager.h"
#include "enb-info.h"
#include "ue-info.h"
static ogs_thread_t *thread;
static void mme_main(void *data);
@@ -67,7 +70,12 @@ 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);
/* dumpers /enb-info /ue-info */
ogs_metrics_register_enb_info(mme_dump_enb_info);
ogs_metrics_register_ue_info(mme_dump_ue_info);
ogs_metrics_enb_info_set_pager = mme_metrics_enb_info_set_pager;
ogs_metrics_ue_info_set_pager = mme_metrics_ue_info_set_pager;
rv = mme_fd_init();
if (rv != OGS_OK) return OGS_ERROR;

398
src/mme/ue-info.c Normal file
View File

@@ -0,0 +1,398 @@
/*
* 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 MME UEs (LTE) JSON dumper for the Prometheus HTTP server (/ue-info).
* - supi, cm_state, enb_info, location, ambr, slices, pdn_count
* - pager: /ue-info?page=0&page_size=100 (0-based, page=-1 without paging) Default: page=0 page_size=100=MAXSIZE
*
* path: http://MME_IP:9090/ue-info
*
* curl -s "http://127.0.0.2:9090/ue-info?" |jq .
* {
* "items": [
* {
* "supi": "999700000021632",
* "domain": "EPS",
* "rat": "E-UTRA",
* "cm_state": "connected",
* "enb": {
* "ostream_id": 3,
* "mme_ue_ngap_id": 3,
* "ran_ue_ngap_id": 9,
* "enb_id": 264040,
* "cell_id": 67594275
* },
* "location": {
* "tai": {
* "plmn": "99970",
* "tac_hex": "0001",
* "tac": 1
* }
* },
* "ambr": {
* "downlink": 1000000000,
* "uplink": 1000000000
* },
* "pdn": [
* {
* "apn": "internet",
* "qos_flows": [
* {
* "ebi": 5
* }
* ],
* "qci": 9,
* "ebi": 5,
* "bearer_count": 1,
* "pdu_state": "active"
* }
* ],
* "pdn_count": 1
* }
* ],
* "pager": {
* "page": 0,
* "page_size": 100,
* "count": 1
* }
* }
*/
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <inttypes.h>
#include "ogs-core.h"
#include "ogs-proto.h"
#include "mme-context.h"
#include "ue-info.h"
#include "mme-context.h"
#include "metrics/prometheus/pager.h"
#include "metrics/prometheus/json_pager.h"
#include "metrics/ogs-metrics.h"
#include "sbi/openapi/external/cJSON.h"
#ifndef MME_UE_INFO_PAGE_SIZE_DEFAULT
#define MME_UE_INFO_PAGE_SIZE_DEFAULT 100U
#endif
static size_t g_ue_page = SIZE_MAX;
static size_t g_ue_page_size = 0;
void mme_metrics_ue_info_set_pager(size_t page, size_t page_size)
{
g_ue_page = page;
g_ue_page_size = page_size;
}
size_t mme_dump_ue_info(char *buf, size_t buflen)
{
size_t page = g_ue_page;
size_t page_size = g_ue_page_size;
if (page == SIZE_MAX) {
page = 0;
page_size = MME_UE_INFO_PAGE_SIZE_DEFAULT;
} else if (page_size == 0) {
page_size = MME_UE_INFO_PAGE_SIZE_DEFAULT;
} else if (page_size > MME_UE_INFO_PAGE_SIZE_DEFAULT) {
page_size = MME_UE_INFO_PAGE_SIZE_DEFAULT;
}
return mme_dump_ue_info_paged(buf, buflen, page, page_size);
}
static inline const char *cm_state_str(const mme_ue_t *ue)
{
return (ue && ECM_CONNECTED(ue)) ? "connected" : "idle";
}
static cJSON *build_enb(const mme_ue_t *ue)
{
cJSON *enb = cJSON_CreateObject();
if (!enb) return NULL;
/* ostream_id */
if (!cJSON_AddNumberToObject(enb, "ostream_id", (double)ue->enb_ostream_id))
goto end;
/* S1AP IDs */
enb_ue_t *ran = enb_ue_find_by_id(ue->enb_ue_id);
if (ran) {
if (!cJSON_AddNumberToObject(enb, "mme_ue_ngap_id", (double)ran->mme_ue_s1ap_id))
goto end;
if (!cJSON_AddNumberToObject(enb, "ran_ue_ngap_id", (double)ran->enb_ue_s1ap_id))
goto end;
mme_enb_t *enb_obj = mme_enb_find_by_id(ran->enb_id);
if (enb_obj && enb_obj->enb_id_presence) {
if (!cJSON_AddNumberToObject(enb, "enb_id", (double)enb_obj->enb_id))
goto end;
}
/* cell_id: prefer last reported via E-UTRAN_CGI from RAN; else fallback to UEs e_cgi */
uint32_t cell_id = 0;
if (ran->saved.e_cgi.cell_id) cell_id = ran->saved.e_cgi.cell_id;
else if (ue->e_cgi.cell_id) cell_id = ue->e_cgi.cell_id;
if (cell_id) {
if (!cJSON_AddNumberToObject(enb, "cell_id", (double)cell_id))
goto end;
}
}
return enb;
end:
cJSON_Delete(enb);
return NULL;
}
static cJSON *build_location(const mme_ue_t *ue)
{
cJSON *loc = cJSON_CreateObject();
if (!loc) return NULL;
cJSON *tai = cJSON_CreateObject();
if (!tai) goto end;
/* TAI: PLMN + TAC (hex and numeric) */
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
ogs_plmn_id_to_string(&ue->tai.plmn_id, plmn_str);
if (!cJSON_AddStringToObject(tai, "plmn", plmn_str)) { cJSON_Delete(tai); goto end; }
char tac_hex[8];
(void)snprintf(tac_hex, sizeof tac_hex, "%04x", (unsigned)ue->tai.tac);
if (!cJSON_AddStringToObject(tai, "tac_hex", tac_hex)) { cJSON_Delete(tai); goto end; }
if (!cJSON_AddNumberToObject(tai, "tac", (double)ue->tai.tac)) { cJSON_Delete(tai); goto end; }
cJSON_AddItemToObjectCS(loc, "tai", tai);
return loc;
end:
cJSON_Delete(loc);
return NULL;
}
static cJSON *build_ambr(const mme_ue_t *ue)
{
cJSON *ambr = cJSON_CreateObject();
if (!ambr) return NULL;
if (!cJSON_AddNumberToObject(ambr, "downlink", (double)ue->ambr.downlink)) goto end;
if (!cJSON_AddNumberToObject(ambr, "uplink", (double)ue->ambr.uplink)) goto end;
return ambr;
end:
cJSON_Delete(ambr);
return NULL;
}
static cJSON *build_pdn_array(const mme_ue_t *ue)
{
cJSON *arr = cJSON_CreateArray();
if (!arr) return NULL;
mme_sess_t *sess = NULL;
ogs_list_for_each(&(ue->sess_list), sess) {
cJSON *it = cJSON_CreateObject();
if (!it) goto oom_all;
/* APN */
const char *apn = (sess->session && sess->session->name && sess->session->name[0])
? (const char*)sess->session->name : "";
if (apn[0]) {
if (!cJSON_AddStringToObject(it, "apn", apn)) { cJSON_Delete(it); goto oom_all; }
}
/* QoS flows: list EBIs; QCI at session-level if present */
unsigned ebi_root = 0;
unsigned bearer_count = 0;
cJSON *qarr = cJSON_CreateArray();
if (!qarr) { cJSON_Delete(it); goto oom_all; }
mme_bearer_t *b = NULL;
ogs_list_for_each(&((mme_sess_t *)sess)->bearer_list, b) {
if (!b || b->ebi == 0) continue;
bearer_count++;
if (ebi_root == 0 || b->ebi < ebi_root)
ebi_root = (unsigned)b->ebi;
cJSON *q = cJSON_CreateObject();
if (!q) { cJSON_Delete(qarr); cJSON_Delete(it); goto oom_all; }
if (!cJSON_AddNumberToObject(q, "ebi", (double)b->ebi)) { cJSON_Delete(q); cJSON_Delete(qarr); cJSON_Delete(it); goto oom_all; }
cJSON_AddItemToArray(qarr, q);
}
cJSON_AddItemToObjectCS(it, "qos_flows", qarr);
/* Session-level QCI (if known) */
if (sess->session && sess->session->qos.index > 0) {
if (!cJSON_AddNumberToObject(it, "qci", (double)sess->session->qos.index)) { cJSON_Delete(it); goto oom_all; }
}
if (ebi_root) {
if (!cJSON_AddNumberToObject(it, "ebi", (double)ebi_root)) { cJSON_Delete(it); goto oom_all; }
}
if (!cJSON_AddNumberToObject(it, "bearer_count", (double)bearer_count)) { cJSON_Delete(it); goto oom_all; }
const char *state = bearer_count ? "active" : "unknown";
if (!cJSON_AddStringToObject(it, "pdu_state", state)) { cJSON_Delete(it); goto oom_all; }
cJSON_AddItemToArray(arr, it);
}
return arr;
oom_all:
cJSON_Delete(arr);
return NULL;
}
static cJSON *ue_to_json(const mme_ue_t *ue)
{
cJSON *o = cJSON_CreateObject();
if (!o) return NULL;
/* identity */
if (ue->imsi_bcd[0]) {
if (!cJSON_AddStringToObject(o, "supi", ue->imsi_bcd)) goto end;
}
if (!cJSON_AddStringToObject(o, "domain", "EPS")) goto end;
if (!cJSON_AddStringToObject(o, "rat", "E-UTRA")) goto end;
if (!cJSON_AddStringToObject(o, "cm_state", cm_state_str(ue))) goto end;
/* enb */
{
cJSON *enb = build_enb(ue);
if (!enb) goto end;
cJSON_AddItemToObjectCS(o, "enb", enb);
}
/* location */
{
cJSON *loc = build_location(ue);
if (!loc) goto end;
cJSON_AddItemToObjectCS(o, "location", loc);
}
/* ambr */
{
cJSON *ambr = build_ambr(ue);
if (!ambr) goto end;
cJSON_AddItemToObjectCS(o, "ambr", ambr);
}
/* pdn + pdn_count */
{
cJSON *pdn = build_pdn_array(ue);
if (!pdn) goto end;
/* Count PDNs before attaching */
size_t pdn_count = 0;
{
cJSON *it = NULL;
cJSON_ArrayForEach(it, pdn) pdn_count++;
}
cJSON_AddItemToObjectCS(o, "pdn", pdn);
if (!cJSON_AddNumberToObject(o, "pdn_count", (double)pdn_count)) goto end;
}
return o;
end:
cJSON_Delete(o);
return NULL;
}
size_t mme_dump_ue_info_paged(char *buf, size_t buflen, size_t page, size_t page_size)
{
if (!buf || buflen == 0) return 0;
const bool no_paging = (page == SIZE_MAX);
if (!no_paging) {
if (page_size == 0) page_size = MME_UE_INFO_PAGE_SIZE_DEFAULT;
if (page_size > MME_UE_INFO_PAGE_SIZE_DEFAULT) page_size = MME_UE_INFO_PAGE_SIZE_DEFAULT;
} else {
page_size = SIZE_MAX;
page = 0;
}
const size_t start_index = json_pager_safe_start_index(no_paging, page, page_size);
cJSON *root = cJSON_CreateObject();
if (!root) {
if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; }
if (buflen) buf[0] = '\0';
return 0;
}
cJSON *items = cJSON_CreateArray();
if (!items) {
cJSON_Delete(root);
if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; }
if (buflen) buf[0] = '\0';
return 0;
}
size_t idx = 0, emitted = 0;
bool has_next = false, oom = false;
mme_context_t *ctxt = mme_self();
if (!ctxt) {
cJSON_Delete(items);
cJSON_Delete(root);
if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; }
if (buflen) buf[0] = '\0';
return 0;
}
mme_ue_t *ue = NULL;
ogs_list_for_each(&ctxt->mme_ue_list, ue) {
int act = json_pager_advance(no_paging, idx, start_index, emitted, page_size, &has_next);
if (act == 1) { idx++; continue; }
if (act == 2) break;
cJSON *one = ue_to_json(ue);
if (!one) { oom = true; break; }
cJSON_AddItemToArray(items, one);
emitted++;
idx++;
}
/* attach only when array is fully built */
cJSON_AddItemToObjectCS(root, "items", items);
json_pager_add_trailing(root, no_paging, page, page_size, emitted,
has_next && !oom, "/ue-info", oom);
return json_pager_finalize(root, buf, buflen);
}

View File

@@ -17,22 +17,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* /connected-gnbs MME-side JSON exporter (Prometheus HTTP endpoint)
* /ue-info MME-side JSON exporter (Prometheus HTTP endpoint)
*
* License: AGPLv3+
*/
#pragma once
#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);
#ifndef UE_INFO_PAGE_SIZE_DEFAULT
#define UE_INFO_PAGE_SIZE_DEFAULT 100U
#endif
void mme_metrics_ue_info_set_pager(size_t page, size_t page_size);
size_t mme_dump_ue_info(char *buf, size_t buflen);
size_t mme_dump_ue_info_paged(char *buf, size_t buflen, size_t page, size_t page_size);
#ifdef __cplusplus
}

View File

@@ -1,336 +0,0 @@
/*
* 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);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
int n = ogs_vsnprintf(buf + off, buflen - off, fmt, ap);
#pragma GCC diagnostic pop
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;
}

View File

@@ -23,8 +23,10 @@
#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() */
#include "ogs-metrics.h"
#include "metrics/prometheus/json_pager.h"
#include "metrics/prometheus/pager.h"
#include "pdu-info.h"
static ogs_thread_t *thread;
static void smf_main(void *data);
@@ -92,7 +94,9 @@ 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);
/* dumper /pdu-info */
ogs_metrics_register_pdu_info(smf_dump_pdu_info);
smf_register_metrics_pager();
initialized = 1;

View File

@@ -70,7 +70,7 @@ libsmf_sources = files('''
ngap-path.h
local-path.h
metrics.h
connected_ues.h
pdu-info.h
init.c
event.c
@@ -113,7 +113,8 @@ libsmf_sources = files('''
ngap-path.c
local-path.c
metrics.c
connected_ues.c
pdu-info.c
../../lib/metrics/prometheus/json_pager.c
'''.split())
libsmf = static_library('smf',

410
src/smf/pdu-info.c Normal file
View File

@@ -0,0 +1,410 @@
/*
* 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 PDUs JSON dumper for the Prometheus HTTP server (/pdu-info).
* - 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")
* - pager: /pdu-info?page=0&page_size=100 (0-based, page=-1 without paging) Default: page=0 page_size=100=MAXSIZE
*
* path: http://SMF_IP:9090/pdu-info
*
* curl -s "http://127.0.0.4:9090/pdu-info?page_size=1" |jq .
* {
* "items": [
* {
* "supi": "imsi-231510000114763",
* "pdu": [
* {
* "psi": 1,
* "dnn": "internet",
* "ipv4": "10.45.0.11",
* "snssai": {
* "sst": 1,
* "sd": "ffffff"
* },
* "qos_flows": [
* {
* "qfi": 1,
* "5qi": 9
* }
* ],
* "pdu_state": "inactive"
* }
* ],
* "ue_activity": "idle"
* }
* ],
* "pager": {
* "page": 0,
* "page_size": 100,
* "count": 1,
* }
* }
*/
#include <string.h>
#include <stdbool.h>
#include <limits.h>
#include "ogs-core.h"
#include "context.h"
#include "pdu-info.h"
#include "metrics/prometheus/pager.h"
#include "sbi/openapi/external/cJSON.h"
#include "metrics/prometheus/json_pager.h"
static size_t g_page = SIZE_MAX;
static size_t g_page_size = 0;
static void smf_metrics_pdu_info_set_pager(size_t page, size_t page_size)
{
g_page = page;
g_page_size = page_size;
}
void smf_register_metrics_pager(void)
{
ogs_metrics_pdu_info_set_pager = smf_metrics_pdu_info_set_pager;
}
static inline uint32_t u24_to_u32(ogs_uint24_t v)
{
uint32_t x = 0;
memcpy(&x, &v, sizeof(v) < sizeof(x) ? sizeof(v) : sizeof(x));
return (x & 0xFFFFFFu);
}
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;
}
/* 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(s->s_nssai.sd) != 0) return true;
if (bearer_list_has_qfi(s)) return true;
return false;
}
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 state at SMF scope: unknown */
static const char *pdu_state_from_lte(const smf_sess_t *sess)
{
(void)sess;
return "unknown";
}
static cJSON *build_snssai_object(const smf_sess_t *sess)
{
cJSON *sn = cJSON_CreateObject();
if (!sn) return NULL;
cJSON *sst = cJSON_CreateNumber((double)sess->s_nssai.sst);
if (!sst) { cJSON_Delete(sn); return NULL; }
cJSON_AddItemToObjectCS(sn, "sst", sst);
char sd[7];
snprintf(sd, sizeof sd, "%06x", (unsigned)u24_to_u32(sess->s_nssai.sd));
cJSON *sdj = cJSON_CreateString(sd);
if (!sdj) { cJSON_Delete(sn); return NULL; }
cJSON_AddItemToObjectCS(sn, "sd", sdj);
return sn;
}
static cJSON *build_qos_flows_array_5g(const smf_sess_t *sess)
{
cJSON *arr = cJSON_CreateArray();
if (!arr) return NULL;
smf_bearer_t *b = NULL;
ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b) {
if (!b || b->qfi == 0) continue;
cJSON *q = cJSON_CreateObject();
if (!q) { cJSON_Delete(arr); return NULL; }
cJSON *qfi = cJSON_CreateNumber((double)(unsigned)b->qfi);
if (!qfi) { cJSON_Delete(q); cJSON_Delete(arr); return NULL; }
cJSON_AddItemToObjectCS(q, "qfi", qfi);
if (b->qos.index > 0) {
cJSON *q5 = cJSON_CreateNumber((double)(unsigned)b->qos.index);
if (!q5) { cJSON_Delete(q); cJSON_Delete(arr); return NULL; }
cJSON_AddItemToObjectCS(q, "5qi", q5);
}
cJSON_AddItemToArray(arr, q);
}
return arr;
}
static cJSON *build_qos_flows_array_lte(const smf_sess_t *sess)
{
cJSON *arr = cJSON_CreateArray();
if (!arr) return NULL;
smf_bearer_t *b = NULL;
ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b) {
if (!b || b->ebi == 0) continue;
unsigned qci_val = (unsigned)b->qos.index;
if (qci_val == 0) qci_val = (unsigned)sess->session.qos.index;
cJSON *q = cJSON_CreateObject();
if (!q) { cJSON_Delete(arr); return NULL; }
cJSON *ebi = cJSON_CreateNumber((double)(unsigned)b->ebi);
if (!ebi) { cJSON_Delete(q); cJSON_Delete(arr); return NULL; }
cJSON_AddItemToObjectCS(q, "ebi", ebi);
if (qci_val > 0) {
cJSON *qci = cJSON_CreateNumber((double)qci_val);
if (!qci) { cJSON_Delete(q); cJSON_Delete(arr); return NULL; }
cJSON_AddItemToObjectCS(q, "qci", qci);
}
cJSON_AddItemToArray(arr, q);
}
return arr;
}
static cJSON *build_single_pdu_object(const smf_sess_t *sess, int *any_active, int *any_unknown)
{
cJSON *pdu = cJSON_CreateObject();
if (!pdu) return NULL;
/* 5G vs LTE fields */
const bool is5g = looks_5g_sess(sess);
if (is5g) {
cJSON *psi = cJSON_CreateNumber((double)(unsigned)sess->psi);
if (!psi) { cJSON_Delete(pdu); return NULL; }
cJSON_AddItemToObjectCS(pdu, "psi", psi);
const char *dnn_c = (sess->session.name ? sess->session.name : "");
cJSON *dnn = cJSON_CreateString(dnn_c);
if (!dnn) { cJSON_Delete(pdu); return NULL; }
cJSON_AddItemToObjectCS(pdu, "dnn", dnn);
} else {
if (sess->psi > 0) {
cJSON *psi = cJSON_CreateNumber((double)(unsigned)sess->psi);
if (!psi) { cJSON_Delete(pdu); return NULL; }
cJSON_AddItemToObjectCS(pdu, "psi", psi);
}
/* EBI root if present */
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; }
}
cJSON *ebi = cJSON_CreateNumber((double)ebi_root);
if (!ebi) { cJSON_Delete(pdu); return NULL; }
cJSON_AddItemToObjectCS(pdu, "ebi", ebi);
const char *apn_c = (sess->session.name ? sess->session.name : "");
cJSON *apn = cJSON_CreateString(apn_c);
if (!apn) { cJSON_Delete(pdu); return NULL; }
cJSON_AddItemToObjectCS(pdu, "apn", apn);
}
/* IPs */
{
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]) {
cJSON *s = cJSON_CreateString(ip4);
if (!s) { cJSON_Delete(pdu); return NULL; }
cJSON_AddItemToObjectCS(pdu, "ipv4", s);
}
if (ip6[0]) {
cJSON *s = cJSON_CreateString(ip6);
if (!s) { cJSON_Delete(pdu); return NULL; }
cJSON_AddItemToObjectCS(pdu, "ipv6", s);
}
}
/* S-NSSAI */
{
cJSON *sn = build_snssai_object(sess);
if (!sn) { cJSON_Delete(pdu); return NULL; }
cJSON_AddItemToObjectCS(pdu, "snssai", sn);
}
/* QoS flows */
{
cJSON *qarr = is5g ? build_qos_flows_array_5g(sess)
: build_qos_flows_array_lte(sess);
if (!qarr) { cJSON_Delete(pdu); return NULL; }
cJSON_AddItemToObjectCS(pdu, "qos_flows", qarr);
}
/* PDU state + UE activity aggregation */
{
const char *state = is5g ? pdu_state_from_5g(sess) : pdu_state_from_lte(sess);
cJSON *st = cJSON_CreateString(state);
if (!st) { cJSON_Delete(pdu); return NULL; }
cJSON_AddItemToObjectCS(pdu, "pdu_state", st);
if (any_active && !strcmp(state, "active")) *any_active = 1;
else if (any_unknown && !strcmp(state, "unknown")) *any_unknown = 1;
}
return pdu;
}
static cJSON *build_ue_object(const smf_ue_t *ue)
{
cJSON *ueo = cJSON_CreateObject();
if (!ueo) return NULL;
/* UE identity */
const char *id = (ue->supi && ue->supi[0]) ? ue->supi :
(ue->imsi_bcd[0] ? ue->imsi_bcd : "");
cJSON *idj = cJSON_CreateString(id);
if (!idj) { cJSON_Delete(ueo); return NULL; }
cJSON_AddItemToObjectCS(ueo, "supi", idj);
/* PDUs */
cJSON *pdus = cJSON_CreateArray();
if (!pdus) { cJSON_Delete(ueo); return NULL; }
int any_active = 0, any_unknown = 0;
smf_sess_t *sess = NULL;
ogs_list_for_each(&ue->sess_list, sess) {
cJSON *pdu = build_single_pdu_object(sess, &any_active, &any_unknown);
if (!pdu) { cJSON_Delete(pdus); cJSON_Delete(ueo); return NULL; }
cJSON_AddItemToArray(pdus, pdu);
}
cJSON_AddItemToObjectCS(ueo, "pdu", pdus);
/* UE activity */
{
const char *ue_act = any_active ? "active" : (any_unknown ? "unknown" : "idle");
cJSON *ua = cJSON_CreateString(ue_act);
if (!ua) { cJSON_Delete(ueo); return NULL; }
cJSON_AddItemToObjectCS(ueo, "ue_activity", ua);
}
return ueo;
}
size_t smf_dump_pdu_info_paged(char *buf, size_t buflen, size_t page, size_t page_size)
{
if (!buf || buflen == 0) return 0;
const bool no_paging = (page == SIZE_MAX);
if (!no_paging) {
if (page_size == 0) page_size = PDU_INFO_PAGE_SIZE_DEFAULT;
if (page_size > PDU_INFO_PAGE_SIZE_DEFAULT) page_size = PDU_INFO_PAGE_SIZE_DEFAULT;
} else {
page_size = SIZE_MAX;
page = 0;
}
const size_t start_index = json_pager_safe_start_index(no_paging, page, page_size);
cJSON *root = cJSON_CreateObject();
if (!root) { if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; } if (buflen) buf[0] = '\0'; return 0; }
cJSON *items = cJSON_CreateArray();
if (!items) { cJSON_Delete(root); if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; } if (buflen) buf[0] = '\0'; return 0; }
size_t idx = 0, emitted = 0;
bool has_next = false, oom = false;
smf_context_t *smf = smf_self();
smf_ue_t *ue = NULL;
ogs_list_for_each(&smf->smf_ue_list, ue) {
int act = json_pager_advance(no_paging, idx, start_index, emitted, page_size, &has_next);
if (act == 1) { idx++; continue; }
if (act == 2) break;
cJSON *ueo = build_ue_object(ue);
if (!ueo) { oom = true; break; }
cJSON_AddItemToArray(items, ueo);
emitted++;
idx++;
}
cJSON_AddItemToObjectCS(root, "items", items);
json_pager_add_trailing(root, no_paging, page, page_size, emitted, has_next && !oom, "/pdu-info", oom);
return json_pager_finalize(root, buf, buflen);
}
size_t smf_dump_pdu_info(char *buf, size_t buflen)
{
size_t page = g_page;
size_t page_size = g_page_size;
if (page == SIZE_MAX) {
page = 0;
page_size = PDU_INFO_PAGE_SIZE_DEFAULT;
} else if (page_size == 0) {
page_size = PDU_INFO_PAGE_SIZE_DEFAULT;
}
return smf_dump_pdu_info_paged(buf, buflen, page, page_size);
}

View File

@@ -18,10 +18,10 @@
*/
/*
* Minimal public API for connected_ues
* Minimal public API for /pdu-info
*/
#ifndef SMF_CONNECTED_UES_H
#define SMF_CONNECTED_UES_H
#ifndef SMF_PDU_INFO_H
#define SMF_PDU_INFO_H
#include <stddef.h>
@@ -29,14 +29,16 @@
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);
#ifndef PDU_INFO_PAGE_SIZE_DEFAULT
#define PDU_INFO_PAGE_SIZE_DEFAULT 100U
#endif
void smf_register_metrics_pager(void);
size_t smf_dump_pdu_info(char *buf, size_t buflen);
size_t smf_dump_pdu_info_paged(char *buf, size_t buflen, size_t page, size_t page_size);
#ifdef __cplusplus
}
#endif
#endif /* SMF_CONNECTED_UES_H */
#endif