From 794c63276e82d4b4c48a0e34d539921f8e514513 Mon Sep 17 00:00:00 2001 From: Juraj Elias <130478827+hug0lin@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:52:54 +0200 Subject: [PATCH] Open5GS JSON API for accessing UE, gNB, eNB, PDU data (#4093) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- lib/metrics/context.c | 24 +- lib/metrics/ogs-metrics.h | 19 +- lib/metrics/prometheus/context.c | 69 ++- lib/metrics/prometheus/json_pager.c | 121 +++++ lib/metrics/prometheus/json_pager.h | 69 +++ lib/metrics/prometheus/pager.h | 38 ++ src/amf/connected_gnbs.c | 250 ---------- src/amf/gnb-info.c | 286 +++++++++++ src/amf/gnb-info.h | 43 ++ src/amf/init.c | 12 +- src/amf/meson.build | 4 +- src/amf/ue-info.c | 633 ++++++++++++++++++++++++ src/amf/{connected_gnbs.h => ue-info.h} | 13 +- src/mme/connected_enbs.c | 190 ------- src/mme/enb-info.c | 250 ++++++++++ src/mme/enb-info.h | 43 ++ src/mme/meson.build | 8 +- src/mme/mme-init.c | 12 +- src/mme/ue-info.c | 398 +++++++++++++++ src/mme/{connected_enbs.h => ue-info.h} | 15 +- src/smf/connected_ues.c | 336 ------------- src/smf/init.c | 10 +- src/smf/meson.build | 5 +- src/smf/pdu-info.c | 410 +++++++++++++++ src/smf/{connected_ues.h => pdu-info.h} | 20 +- 25 files changed, 2437 insertions(+), 841 deletions(-) create mode 100644 lib/metrics/prometheus/json_pager.c create mode 100644 lib/metrics/prometheus/json_pager.h create mode 100644 lib/metrics/prometheus/pager.h delete mode 100644 src/amf/connected_gnbs.c create mode 100644 src/amf/gnb-info.c create mode 100644 src/amf/gnb-info.h create mode 100644 src/amf/ue-info.c rename src/amf/{connected_gnbs.h => ue-info.h} (71%) delete mode 100644 src/mme/connected_enbs.c create mode 100644 src/mme/enb-info.c create mode 100644 src/mme/enb-info.h create mode 100644 src/mme/ue-info.c rename src/mme/{connected_enbs.h => ue-info.h} (71%) delete mode 100644 src/smf/connected_ues.c create mode 100644 src/smf/pdu-info.c rename src/smf/{connected_ues.h => pdu-info.h} (70%) diff --git a/lib/metrics/context.c b/lib/metrics/context.c index 8b6327cc2..fa0399b23 100644 --- a/lib/metrics/context.c +++ b/lib/metrics/context.c @@ -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; diff --git a/lib/metrics/ogs-metrics.h b/lib/metrics/ogs-metrics.h index a45713d28..d96617c55 100644 --- a/lib/metrics/ogs-metrics.h +++ b/lib/metrics/ogs-metrics.h @@ -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 } diff --git a/lib/metrics/prometheus/context.c b/lib/metrics/prometheus/context.c index b68106157..59348353b 100644 --- a/lib/metrics/prometheus/context.c +++ b/lib/metrics/prometheus/context.c @@ -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 +#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); @@ -290,23 +308,46 @@ mhd_server_access_handler(void *cls, struct MHD_Connection *connection, MHD_destroy_response(rsp); 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 */ diff --git a/lib/metrics/prometheus/json_pager.c b/lib/metrics/prometheus/json_pager.c new file mode 100644 index 000000000..d2e8153e5 --- /dev/null +++ b/lib/metrics/prometheus/json_pager.c @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/* + * /ue-info — MME-side JSON exporter (Prometheus HTTP endpoint) + * + * License: AGPLv3+ + */ + +/* lib/metrics/prometheus/json_pager.c */ + +#include +#include +#include +#include + +#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; +} + diff --git a/lib/metrics/prometheus/json_pager.h b/lib/metrics/prometheus/json_pager.h new file mode 100644 index 000000000..9d458f5f7 --- /dev/null +++ b/lib/metrics/prometheus/json_pager.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/* + * /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 +#include + +/* 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 */ + + diff --git a/lib/metrics/prometheus/pager.h b/lib/metrics/prometheus/pager.h new file mode 100644 index 000000000..b395c05d6 --- /dev/null +++ b/lib/metrics/prometheus/pager.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef OGS_METRICS_PROM_PAGER_H +#define OGS_METRICS_PROM_PAGER_H + +#include + +/* 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 */ + diff --git a/src/amf/connected_gnbs.c b/src/amf/connected_gnbs.c deleted file mode 100644 index e739a6066..000000000 --- a/src/amf/connected_gnbs.c +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright (C) 2025 by Juraj Elias - * - * This file is part of Open5GS. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/* - * JSON dumper for /connected-gnbs (AMF) - * Output (one item per connected gNB): - * [ - * { - * "gnb_id": 100, - * "plmn": "99970", - * "network": { - * "amf_name": "efire-amf0", - * "ngap_port": 38412 - * }, - * "ng": { - * "setup_success": true, - * "sctp": { - * "peer": "[192.168.168.100]:60110", - * "max_out_streams": 2, - * "next_ostream_id": 1 - * } - * }, - * "supported_ta_list": [ - * { - * "tac": "000001", - * "bplmns": [ - * { - * "plmn": "99970", - * "snssai": [ - * { - * "sst": 1, - * "sd": "ffffff" - * } - * ] - * } - * ] - * } - * ], - * "num_connected_ues": 1 - * } - * ] - */ - -/* - * Copyright (C) 2025 by Juraj Elias - * JSON dumper for /connected-gnbs (AMF) - */ - -#include -#include -#include -#include -#include - -#include "context.h" -#include "ogs-proto.h" -#include "ogs-core.h" - -/* Exported */ -size_t amf_dump_connected_gnbs(char *buf, size_t buflen); - -/* ------------------------- small helpers ------------------------- */ - -static inline size_t append_safe(char *buf, size_t off, size_t buflen, const char *fmt, ...) -{ - if (!buf || off == (size_t)-1 || off >= buflen) return (size_t)-1; - va_list ap; - va_start(ap, fmt); -#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; -} - diff --git a/src/amf/gnb-info.c b/src/amf/gnb-info.c new file mode 100644 index 000000000..28887af46 --- /dev/null +++ b/src/amf/gnb-info.c @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * 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 +#include +#include +#include + +#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); +} + diff --git a/src/amf/gnb-info.h b/src/amf/gnb-info.h new file mode 100644 index 000000000..7bc4c8de4 --- /dev/null +++ b/src/amf/gnb-info.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * /gnb-info — AMF-side JSON exporter (Prometheus HTTP endpoint) + * + */ +#pragma once + +#include + +#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 + diff --git a/src/amf/init.c b/src/amf/init.c index da354a3dd..ab0783539 100644 --- a/src/amf/init.c +++ b/src/amf/init.c @@ -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; diff --git a/src/amf/meson.build b/src/amf/meson.build index c7f98b748..809d25ee5 100644 --- a/src/amf/meson.build +++ b/src/amf/meson.build @@ -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', diff --git a/src/amf/ue-info.c b/src/amf/ue-info.c new file mode 100644 index 000000000..8c877e7ef --- /dev/null +++ b/src/amf/ue-info.c @@ -0,0 +1,633 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * Connected 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 +#include +#include +#include +#include + +#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); +} + diff --git a/src/amf/connected_gnbs.h b/src/amf/ue-info.h similarity index 71% rename from src/amf/connected_gnbs.h rename to src/amf/ue-info.h index 8cc67dad5..2166ac0ff 100644 --- a/src/amf/connected_gnbs.h +++ b/src/amf/ue-info.h @@ -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 } diff --git a/src/mme/connected_enbs.c b/src/mme/connected_enbs.c deleted file mode 100644 index f026e1730..000000000 --- a/src/mme/connected_enbs.c +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2025 by Juraj Elias - * - * This file is part of Open5GS. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/* - * JSON dumper for /connected-enbs (MME) - * Output (one item per connected eNB): - *[ - * { - * "enb_id": 264040, - * "plmn": "99970", - * "network": { - * "mme_name": "efire-mme0" - * }, - * "s1": { - * "setup_success": true, - * "sctp": { - * "peer": "[192.168.168.254]:36412", - * "max_out_streams": 10, - * "next_ostream_id": 1 - * } - * }, - * "supported_ta_list": [ - * { - * "tac": "000001", - * "plmn": "99970" - * } - * ], - * "num_connected_ues": 1 - * } - *] - */ - -#include -#include -#include -#include - -#include "ogs-core.h" -#include "ogs-proto.h" -#include "ogs-app.h" -#include "mme-context.h" - -/* Exported */ -size_t mme_dump_connected_enbs(char *buf, size_t buflen); - -/* ------------------------- small helpers ------------------------- */ - -static inline size_t append_safe(char *buf, size_t off, size_t buflen, const char *fmt, ...) -{ - if (!buf || off == (size_t)-1 || off >= buflen) return (size_t)-1; - va_list ap; - va_start(ap, fmt); -#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; -} - diff --git a/src/mme/enb-info.c b/src/mme/enb-info.c new file mode 100644 index 000000000..a2586c34e --- /dev/null +++ b/src/mme/enb-info.c @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * 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 +#include +#include +#include + +#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); +} + diff --git a/src/mme/enb-info.h b/src/mme/enb-info.h new file mode 100644 index 000000000..19566e98d --- /dev/null +++ b/src/mme/enb-info.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/* + * /enb-info — MME-side JSON exporter (Prometheus HTTP endpoint) + * + * License: AGPLv3+ + */ +#pragma once + +#include + +#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 + diff --git a/src/mme/meson.build b/src/mme/meson.build index 313821827..bdec057b0 100644 --- a/src/mme/meson.build +++ b/src/mme/meson.build @@ -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(''' diff --git a/src/mme/mme-init.c b/src/mme/mme-init.c index ba0b7f275..7a5638b72 100644 --- a/src/mme/mme-init.c +++ b/src/mme/mme-init.c @@ -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; diff --git a/src/mme/ue-info.c b/src/mme/ue-info.c new file mode 100644 index 000000000..197e5f27a --- /dev/null +++ b/src/mme/ue-info.c @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/* + * + * Connected 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 +#include +#include +#include +#include + +#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 UE’s 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); +} + diff --git a/src/mme/connected_enbs.h b/src/mme/ue-info.h similarity index 71% rename from src/mme/connected_enbs.h rename to src/mme/ue-info.h index 65dba8f57..07bc3e194 100644 --- a/src/mme/connected_enbs.h +++ b/src/mme/ue-info.h @@ -17,22 +17,25 @@ * along with this program. If not, see . */ /* - * /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 #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 } diff --git a/src/smf/connected_ues.c b/src/smf/connected_ues.c deleted file mode 100644 index 9c591fb64..000000000 --- a/src/smf/connected_ues.c +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright (C) 2025 by Juraj Elias - * - * This file is part of Open5GS. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/* - * Connected UEs JSON dumper for the Prometheus HTTP server (/connected-ues). - * - 5G PDUs: psi+dnn, snssai, qos_flows [{qfi,5qi}], pdu_state ("active"/"inactive"/"unknown") - * - LTE PDUs: ebi(+psi if non-zero)+apn, qos_flows [{ebi,qci}], pdu_state ("unknown" at SMF scope) - * - UE-level: ue_activity ("active" if any PDU active; "unknown" if none active but any unknown; else "idle") - */ - -/* - * JSON dumper for /connected-gnbs (AMF) - * Output (one item per connected gNB): - * [ - * { - * "supi": "imsi-999700000083810", - * "pdu": [ - * { - * "psi": 1, - * "dnn": "internet", - * "ipv4": "10.45.0.2", - * "snssai": { - * "sst": 1, - * "sd": "ffffff" - * }, - * "qos_flows": [ - * { - * "qfi": 1, - * "5qi": 9 - * } - * ], - * "pdu_state": "inactive" - * } - * ], - * "ue_activity": "idle" - * }, - * ] - */ -/* - * Copyright (C) 2025 by Juraj Elias - * This file is part of Open5GS (AGPLv3+) - * - * JSON dumper for /connected-ues (SMF) - * - 5G PDUs: psi+dnn, snssai, qos_flows [{qfi,5qi}], pdu_state - * - LTE PDUs: ebi(+psi if non-zero)+apn, qos_flows [{ebi,qci}], pdu_state="unknown" - * - UE-level: ue_activity derived from PDU states - */ - -#include -#include -#include -#include -#include - -#include "ogs-core.h" /* OGS_INET_NTOP, OGS_INET6_NTOP, OGS_ADDRSTRLEN, ogs_uint24_t */ -#include "context.h" /* smf_self(), smf_ue_t, smf_sess_t, smf_bearer_t, ogs_s_nssai_t */ -#include "connected_ues.h" /* size_t smf_dump_connected_ues(char *buf, size_t buflen) */ - -/* ------------------------- small helpers ------------------------- */ - -static inline size_t append_safe(char *buf, size_t off, size_t buflen, const char *fmt, ...) -{ - if (!buf || off == (size_t)-1 || off >= buflen) return (size_t)-1; - va_list ap; - va_start(ap, fmt); -#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; -} - diff --git a/src/smf/init.c b/src/smf/init.c index b359ab91e..df367696f 100644 --- a/src/smf/init.c +++ b/src/smf/init.c @@ -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; diff --git a/src/smf/meson.build b/src/smf/meson.build index 0f5d472d1..b501710e4 100644 --- a/src/smf/meson.build +++ b/src/smf/meson.build @@ -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', diff --git a/src/smf/pdu-info.c b/src/smf/pdu-info.c new file mode 100644 index 000000000..bf7c6bccc --- /dev/null +++ b/src/smf/pdu-info.c @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2025 by Juraj Elias + * + * This file is part of Open5GS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * Connected 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 +#include +#include + +#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); +} + diff --git a/src/smf/connected_ues.h b/src/smf/pdu-info.h similarity index 70% rename from src/smf/connected_ues.h rename to src/smf/pdu-info.h index c7bfe4eb2..ad8c59fb1 100644 --- a/src/smf/connected_ues.h +++ b/src/smf/pdu-info.h @@ -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 @@ -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