mirror of
https://github.com/open5gs/open5gs.git
synced 2025-10-23 07:41:57 +00:00
Open5GS JSON API for accessing UE, gNB, eNB, PDU data (#4093)
Open5GS has a lightweight HTTP server (already used for `/metrics`) embedded in each NF. New optional JSON endpoints were added: | NF | Endpoint | Content | | --- | --- | --- | | **SMF** | `/pdu-info` | All currently connected UEs + their PDU sessions (IMSI/SUPI, DNN, IPs, S-NSSAI, QoS, state, etc.) | | **AMF** | `/gnb-info` | All currently connected gNBs and their supported TAs, PLMNs, SCTP info, number of UEs | | **AMF** | `/ue-info` | All currently connected NR UEs and their info, active gNB, tai, security, slices, am_policy | | **MME** | `/enb-info` | All currently connected eNBs and their supported TAs, PLMNs, SCTP info, number of UEs | | **MME** | `/ue-info` | All currently connected LTE UEs and their info, active eNB, tai, pdn info | They are exposed on the same HTTP port used by Prometheus metrics (default `:9090`). To reduce processor load when there are a large number of devices, the API includes a pager that limits output. `/ue-info?page=0&page_size=100` page in the range 0-n (0 is default), `page=-1` to avoid paging `page_size=100` (default and MAX)
This commit is contained in:
@@ -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;
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -23,9 +23,10 @@
|
||||
* Prometheus HTTP server (MicroHTTPD) with optional JSON endpoints:
|
||||
* - / (provide health check)
|
||||
* - /metrics (provide prometheus metrics metrics according to the relevant NF)
|
||||
* - /connected-ues (provided by NF registering ogs_metrics_connected_ues_dumper)
|
||||
* - /connected-gnbs (provided by NF registering ogs_metrics_connected_gnbs_dumper)
|
||||
* - /connected-enbs (provided by NF registering ogs_metrics_connected_enbs_dumper)
|
||||
* - /pdu-info (provided by NF registering ogs_metrics_pdu_info_dumper)
|
||||
* - /gnb-info (provided by NF registering ogs_metrics_gnb_info_dumper)
|
||||
* - /enb-info (provided by NF registering ogs_metrics_enb_info_dumper)
|
||||
* - /ue-info (provided by NF registering ogs_metrics_ue_info_dumper)
|
||||
*/
|
||||
|
||||
#include "ogs-core.h"
|
||||
@@ -35,6 +36,12 @@
|
||||
#include "prom.h"
|
||||
#include "microhttpd.h"
|
||||
#include <string.h>
|
||||
#include "prometheus/pager.h"
|
||||
|
||||
ogs_metrics_pager_fn ogs_metrics_pdu_info_set_pager = NULL;
|
||||
ogs_metrics_pager_fn ogs_metrics_gnb_info_set_pager = NULL;
|
||||
ogs_metrics_pager_fn ogs_metrics_enb_info_set_pager = NULL;
|
||||
ogs_metrics_pager_fn ogs_metrics_ue_info_set_pager = NULL;
|
||||
|
||||
extern int __ogs_metrics_domain;
|
||||
#define MAX_LABELS 8
|
||||
@@ -75,6 +82,17 @@ static OGS_POOL(metrics_server_pool, ogs_metrics_server_t);
|
||||
static int ogs_metrics_context_server_start(ogs_metrics_server_t *server);
|
||||
static int ogs_metrics_context_server_stop(ogs_metrics_server_t *server);
|
||||
|
||||
static size_t get_query_size_t(struct MHD_Connection *connection,
|
||||
const char *key, size_t default_val)
|
||||
{
|
||||
const char *val = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, key);
|
||||
if (!val || !*val) return default_val;
|
||||
char *end = NULL;
|
||||
unsigned long long v = strtoull(val, &end, 10);
|
||||
if (end == val || *end != '\0') return default_val;
|
||||
return (size_t)v;
|
||||
}
|
||||
|
||||
void ogs_metrics_server_init(ogs_metrics_context_t *ctx)
|
||||
{
|
||||
ogs_list_init(&ctx->server_list);
|
||||
@@ -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 */
|
||||
|
121
lib/metrics/prometheus/json_pager.c
Normal file
121
lib/metrics/prometheus/json_pager.c
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
* /ue-info — MME-side JSON exporter (Prometheus HTTP endpoint)
|
||||
*
|
||||
* License: AGPLv3+
|
||||
*/
|
||||
|
||||
/* lib/metrics/prometheus/json_pager.c */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ogs-core.h"
|
||||
#include "ogs-proto.h"
|
||||
#include "sbi/openapi/external/cJSON.h"
|
||||
|
||||
#include "metrics/prometheus/json_pager.h"
|
||||
|
||||
size_t
|
||||
json_pager_safe_start_index(bool no_paging, size_t page, size_t page_size)
|
||||
{
|
||||
if (no_paging) return 0;
|
||||
if (page_size != 0 && page > SIZE_MAX / page_size)
|
||||
return SIZE_MAX; /* treat as beyond end */
|
||||
return page * page_size;
|
||||
}
|
||||
|
||||
void
|
||||
json_pager_add_trailing(cJSON *root,
|
||||
bool no_paging,
|
||||
size_t page,
|
||||
size_t page_size,
|
||||
size_t count,
|
||||
bool has_next,
|
||||
const char *base_path,
|
||||
bool truncated)
|
||||
{
|
||||
cJSON *pager = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(pager, "page", (double)(no_paging ? 0 : page));
|
||||
cJSON_AddNumberToObject(pager, "page_size", (double)(no_paging ? 0 : page_size));
|
||||
cJSON_AddNumberToObject(pager, "count", (double)count);
|
||||
|
||||
/* Only add "truncated" when true */
|
||||
if (truncated)
|
||||
cJSON_AddBoolToObject(pager, "truncated", 1);
|
||||
|
||||
if (!no_paging) {
|
||||
if (page > 0) {
|
||||
char prev[128];
|
||||
snprintf(prev, sizeof prev, "%s?page=%zu", base_path, page - 1);
|
||||
cJSON_AddStringToObject(pager, "prev", prev);
|
||||
}
|
||||
if (has_next) {
|
||||
char next[128];
|
||||
snprintf(next, sizeof next, "%s?page=%zu", base_path, page + 1);
|
||||
cJSON_AddStringToObject(pager, "next", next);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "pager", pager);
|
||||
}
|
||||
|
||||
size_t
|
||||
json_pager_finalize(cJSON *root, char *buf, size_t buflen)
|
||||
{
|
||||
size_t outlen = 0;
|
||||
|
||||
if (cJSON_PrintPreallocated(root, buf, (int)buflen, 0)) {
|
||||
outlen = strlen(buf);
|
||||
cJSON_Delete(root);
|
||||
return outlen;
|
||||
}
|
||||
|
||||
char *tmp = cJSON_PrintUnformatted(root);
|
||||
if (!tmp) {
|
||||
if (buflen >= 3) {
|
||||
memcpy(buf, "{}", 3);
|
||||
cJSON_Delete(root);
|
||||
return 2;
|
||||
}
|
||||
if (buflen) buf[0] = '\0';
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t need = strlen(tmp);
|
||||
if (need + 1 <= buflen) {
|
||||
memcpy(buf, tmp, need + 1);
|
||||
outlen = need;
|
||||
} else {
|
||||
if (buflen >= 3) {
|
||||
memcpy(buf, "{}", 3);
|
||||
outlen = 2;
|
||||
} else if (buflen) {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
}
|
||||
cJSON_free(tmp);
|
||||
cJSON_Delete(root);
|
||||
return outlen;
|
||||
}
|
||||
|
69
lib/metrics/prometheus/json_pager.h
Normal file
69
lib/metrics/prometheus/json_pager.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
* /ue-info — MME-side JSON exporter (Prometheus HTTP endpoint)
|
||||
*
|
||||
* License: AGPLv3+
|
||||
*/
|
||||
|
||||
/* lib/metrics/prometheus/json_pager.h */
|
||||
|
||||
#ifndef OGS_METRICS_PROM_JSON_PAGER_H
|
||||
#define OGS_METRICS_PROM_JSON_PAGER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Forward-declare so callers don't need to include cJSON.h here */
|
||||
typedef struct cJSON cJSON;
|
||||
|
||||
/* Safe start_index = page * page_size with overflow guard. */
|
||||
size_t json_pager_safe_start_index(bool no_paging, size_t page, size_t page_size);
|
||||
|
||||
/* Append trailing { "pager": { page, page_size, count, [truncated], prev?, next? } }. */
|
||||
void json_pager_add_trailing(cJSON *root,
|
||||
bool no_paging,
|
||||
size_t page,
|
||||
size_t page_size,
|
||||
size_t count,
|
||||
bool has_next,
|
||||
const char *base_path,
|
||||
bool truncated);
|
||||
|
||||
/* Finalize JSON into buf and free root. */
|
||||
size_t json_pager_finalize(cJSON *root, char *buf, size_t buflen);
|
||||
|
||||
/* Paging step helper
|
||||
* Returns: 1 -> CONTINUE (skip), 2 -> BREAK (page full; set has_next), 0 -> EMIT.
|
||||
*/
|
||||
static inline int
|
||||
json_pager_advance(bool no_paging,
|
||||
size_t idx, size_t start_index,
|
||||
size_t emitted, size_t page_size,
|
||||
bool *has_next)
|
||||
{
|
||||
if (no_paging) return 0;
|
||||
if (idx < start_index) return 1;
|
||||
if (emitted == page_size) { if (has_next) *has_next = true; return 2; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* OGS_METRICS_PROM_JSON_PAGER_H */
|
||||
|
||||
|
38
lib/metrics/prometheus/pager.h
Normal file
38
lib/metrics/prometheus/pager.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef OGS_METRICS_PROM_PAGER_H
|
||||
#define OGS_METRICS_PROM_PAGER_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* NF-specific optional pager setter signatures.
|
||||
* NFs may assign these at init time. If left NULL, paging is ignored for that NF.
|
||||
*/
|
||||
|
||||
typedef void (*ogs_metrics_pager_fn)(size_t page, size_t page_size);
|
||||
|
||||
/* Connected PDUs (SMF) */
|
||||
extern ogs_metrics_pager_fn ogs_metrics_pdu_info_set_pager;
|
||||
extern ogs_metrics_pager_fn ogs_metrics_ue_info_set_pager;
|
||||
extern ogs_metrics_pager_fn ogs_metrics_gnb_info_set_pager;
|
||||
extern ogs_metrics_pager_fn ogs_metrics_enb_info_set_pager;
|
||||
|
||||
#endif /* OGS_METRICS_PROM_PAGER_H */
|
||||
|
@@ -1,250 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* JSON dumper for /connected-gnbs (AMF)
|
||||
* Output (one item per connected gNB):
|
||||
* [
|
||||
* {
|
||||
* "gnb_id": 100,
|
||||
* "plmn": "99970",
|
||||
* "network": {
|
||||
* "amf_name": "efire-amf0",
|
||||
* "ngap_port": 38412
|
||||
* },
|
||||
* "ng": {
|
||||
* "setup_success": true,
|
||||
* "sctp": {
|
||||
* "peer": "[192.168.168.100]:60110",
|
||||
* "max_out_streams": 2,
|
||||
* "next_ostream_id": 1
|
||||
* }
|
||||
* },
|
||||
* "supported_ta_list": [
|
||||
* {
|
||||
* "tac": "000001",
|
||||
* "bplmns": [
|
||||
* {
|
||||
* "plmn": "99970",
|
||||
* "snssai": [
|
||||
* {
|
||||
* "sst": 1,
|
||||
* "sd": "ffffff"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ],
|
||||
* "num_connected_ues": 1
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias
|
||||
* JSON dumper for /connected-gnbs (AMF)
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "context.h"
|
||||
#include "ogs-proto.h"
|
||||
#include "ogs-core.h"
|
||||
|
||||
/* Exported */
|
||||
size_t amf_dump_connected_gnbs(char *buf, size_t buflen);
|
||||
|
||||
/* ------------------------- small helpers ------------------------- */
|
||||
|
||||
static inline size_t append_safe(char *buf, size_t off, size_t buflen, const char *fmt, ...)
|
||||
{
|
||||
if (!buf || off == (size_t)-1 || off >= buflen) return (size_t)-1;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
|
||||
int n = ogs_vsnprintf(buf + off, buflen - off, fmt, ap);
|
||||
#pragma GCC diagnostic pop
|
||||
va_end(ap);
|
||||
if (n < 0 || (size_t)n >= buflen - off) return (size_t)-1;
|
||||
return off + (size_t)n;
|
||||
}
|
||||
|
||||
static size_t append_json_kv_escaped(char *buf, size_t off, size_t buflen,
|
||||
const char *key, const char *val)
|
||||
{
|
||||
if (!val) val = "";
|
||||
off = append_safe(buf, off, buflen, "\"%s\":\"", key);
|
||||
if (off == (size_t)-1) return off;
|
||||
for (const unsigned char *p = (const unsigned char *)val; *p; ++p) {
|
||||
unsigned char c = *p;
|
||||
if (c == '\\' || c == '\"') off = append_safe(buf, off, buflen, "\\%c", c);
|
||||
else if (c < 0x20) off = append_safe(buf, off, buflen, "\\u%04x", (unsigned)c);
|
||||
else off = append_safe(buf, off, buflen, "%c", c);
|
||||
if (off == (size_t)-1) return off;
|
||||
}
|
||||
return append_safe(buf, off, buflen, "\"");
|
||||
}
|
||||
|
||||
/* "plmn":"XXXXX" */
|
||||
static size_t append_plmn_kv(char *buf, size_t off, size_t buflen, const ogs_plmn_id_t *plmn)
|
||||
{
|
||||
char s[OGS_PLMNIDSTRLEN] = {0};
|
||||
ogs_plmn_id_to_string(plmn, s);
|
||||
return append_safe(buf, off, buflen, "\"plmn\":\"%s\"", s);
|
||||
}
|
||||
|
||||
/* 24-bit helpers */
|
||||
static inline uint32_t u24_to_u32_portable(ogs_uint24_t v)
|
||||
{
|
||||
uint32_t x = 0; memcpy(&x, &v, sizeof(v) < sizeof(x) ? sizeof(v) : sizeof(x));
|
||||
return (x & 0xFFFFFFu);
|
||||
}
|
||||
|
||||
static size_t append_u24_hex6(char *buf, size_t off, size_t buflen, const ogs_uint24_t v)
|
||||
{
|
||||
uint32_t u = u24_to_u32_portable(v);
|
||||
return append_safe(buf, off, buflen, "\"%06x\"", (unsigned)(u & 0xFFFFFFu));
|
||||
}
|
||||
|
||||
/* S-NSSAI */
|
||||
static size_t append_snssai_obj(char *buf, size_t off, size_t buflen, const ogs_s_nssai_t *sn)
|
||||
{
|
||||
unsigned sst = (unsigned)sn->sst;
|
||||
uint32_t sd_u32 = u24_to_u32_portable(sn->sd);
|
||||
off = append_safe(buf, off, buflen, "{");
|
||||
off = append_safe(buf, off, buflen, "\"sst\":%u", sst);
|
||||
off = append_safe(buf, off, buflen, ",\"sd\":\"%06x\"}", (unsigned)(sd_u32 & 0xFFFFFFu));
|
||||
return off;
|
||||
}
|
||||
|
||||
static size_t append_snssai_arr(char *buf, size_t off, size_t buflen,
|
||||
const ogs_s_nssai_t *arr, int n)
|
||||
{
|
||||
off = append_safe(buf, off, buflen, "[");
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i) off = append_safe(buf, off, buflen, ",");
|
||||
off = append_snssai_obj(buf, off, buflen, &arr[i]);
|
||||
}
|
||||
off = append_safe(buf, off, buflen, "]");
|
||||
return off;
|
||||
}
|
||||
|
||||
/* sockaddr -> string */
|
||||
static inline const char *safe_sa_str(const ogs_sockaddr_t *sa)
|
||||
{
|
||||
if (!sa) return "";
|
||||
int fam = ((const struct sockaddr *)&sa->sa)->sa_family;
|
||||
if (fam != AF_INET && fam != AF_INET6) return "";
|
||||
return ogs_sockaddr_to_string_static((ogs_sockaddr_t *)sa);
|
||||
}
|
||||
|
||||
/* UE counter on this gNB */
|
||||
static size_t count_connected_ues_for_gnb(const amf_gnb_t *gnb)
|
||||
{
|
||||
size_t total = 0; ran_ue_t *r = NULL;
|
||||
ogs_list_for_each(&((amf_gnb_t*)gnb)->ran_ue_list, r) total++;
|
||||
return total;
|
||||
}
|
||||
|
||||
#define APPF(...) do { off = append_safe(buf, off, buflen, __VA_ARGS__); if (off==(size_t)-1) goto trunc; } while(0)
|
||||
#define APPX(expr) do { off = (expr); if (off==(size_t)-1) goto trunc; } while(0)
|
||||
|
||||
/* --------------------------- main --------------------------- */
|
||||
|
||||
size_t amf_dump_connected_gnbs(char *buf, size_t buflen)
|
||||
{
|
||||
size_t off = 0;
|
||||
amf_context_t *ctxt = amf_self();
|
||||
ogs_assert(ctxt);
|
||||
amf_gnb_t *gnb = NULL;
|
||||
|
||||
APPF("[");
|
||||
bool first_gnb = true;
|
||||
|
||||
ogs_list_for_each(&ctxt->gnb_list, gnb) {
|
||||
if (!first_gnb) APPF(",");
|
||||
first_gnb = false;
|
||||
|
||||
size_t num_total = count_connected_ues_for_gnb(gnb);
|
||||
|
||||
APPF("{");
|
||||
|
||||
APPF("\"gnb_id\":%u", (unsigned)gnb->gnb_id);
|
||||
APPF(",");
|
||||
APPX(append_plmn_kv(buf, off, buflen, &gnb->plmn_id));
|
||||
|
||||
APPF(",\"network\":{");
|
||||
APPX(append_json_kv_escaped(buf, off, buflen, "amf_name", ctxt->amf_name ? ctxt->amf_name : ""));
|
||||
APPF(",\"ngap_port\":%u", (unsigned)ctxt->ngap_port);
|
||||
APPF("}");
|
||||
|
||||
APPF(",\"ng\":{");
|
||||
APPF("\"setup_success\":%s", gnb->state.ng_setup_success ? "true" : "false");
|
||||
APPF(",\"sctp\":{");
|
||||
APPF("\"peer\":\"%s\"", safe_sa_str(gnb->sctp.addr));
|
||||
APPF(",\"max_out_streams\":%d", gnb->max_num_of_ostreams);
|
||||
APPF(",\"next_ostream_id\":%u", (unsigned)gnb->ostream_id);
|
||||
APPF("}");
|
||||
APPF("}");
|
||||
|
||||
APPF(",\"supported_ta_list\":[");
|
||||
for (int t = 0; t < gnb->num_of_supported_ta_list; t++) {
|
||||
if (t) APPF(",");
|
||||
APPF("{");
|
||||
APPF("\"tac\":");
|
||||
APPX(append_u24_hex6(buf, off, buflen, gnb->supported_ta_list[t].tac));
|
||||
|
||||
APPF(",\"bplmns\":[");
|
||||
for (int p = 0; p < gnb->supported_ta_list[t].num_of_bplmn_list; p++) {
|
||||
if (p) APPF(",");
|
||||
const ogs_plmn_id_t *bp_plmn = &gnb->supported_ta_list[t].bplmn_list[p].plmn_id;
|
||||
const int ns = gnb->supported_ta_list[t].bplmn_list[p].num_of_s_nssai;
|
||||
const ogs_s_nssai_t *sn = gnb->supported_ta_list[t].bplmn_list[p].s_nssai;
|
||||
|
||||
APPF("{");
|
||||
APPX(append_plmn_kv(buf, off, buflen, bp_plmn));
|
||||
APPF(",\"snssai\":");
|
||||
APPX(append_snssai_arr(buf, off, buflen, sn, ns));
|
||||
APPF("}");
|
||||
}
|
||||
APPF("]");
|
||||
APPF("}");
|
||||
}
|
||||
APPF("]");
|
||||
|
||||
APPF(",\"num_connected_ues\":%zu", num_total);
|
||||
|
||||
APPF("}");
|
||||
}
|
||||
|
||||
APPF("]");
|
||||
return off;
|
||||
|
||||
trunc:
|
||||
if (buf && buflen >= 3) { buf[0]='['; buf[1]=']'; buf[2]='\0'; return 2; }
|
||||
if (buf && buflen) buf[0]='\0';
|
||||
return 0;
|
||||
}
|
||||
|
286
src/amf/gnb-info.c
Normal file
286
src/amf/gnb-info.c
Normal file
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* GNB info - AMF gNBs JSON dumper for the Prometheus HTTP server (/gnb-info).
|
||||
* - gnb_id, plmn, amf, ng, gnb_IP, supported ta_list, slices, num_connceted_ues
|
||||
* - pager: /gnb-info?page=0&page_size=100 (0-based, page=-1 without paging) Default: page=0 page_size=100=MAXSIZE
|
||||
*
|
||||
* path: http://AMF_IP:9090/gnb-info
|
||||
* curl -s "http://127.0.0.5:9090/gnb-info?" |jq .
|
||||
* {
|
||||
* "items": [
|
||||
* {
|
||||
* "gnb_id": 100,
|
||||
* "plmn": "99970",
|
||||
* "network": {
|
||||
* "amf_name": "efire-amf0",
|
||||
* "ngap_port": 38412
|
||||
* },
|
||||
* "ng": {
|
||||
* "sctp": {
|
||||
* "peer": "[192.168.168.100]:60110",
|
||||
* "max_out_streams": 2,
|
||||
* "next_ostream_id": 1
|
||||
* },
|
||||
* "setup_success": true
|
||||
* },
|
||||
* "supported_ta_list": [
|
||||
* {
|
||||
* "tac": "000001",
|
||||
* "bplmns": [
|
||||
* {
|
||||
* "plmn": "99970",
|
||||
* "snssai": [
|
||||
* {
|
||||
* "sst": 1,
|
||||
* "sd": "ffffff"
|
||||
* }
|
||||
* ]
|
||||
* },
|
||||
* {
|
||||
* "plmn": "99971",
|
||||
* "snssai": [
|
||||
* {
|
||||
* "sst": 2,
|
||||
* "sd": "000000"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ],
|
||||
* "num_connected_ues": 2
|
||||
* }
|
||||
* ],
|
||||
* "pager": {
|
||||
* "page": 0,
|
||||
* "page_size": 100,
|
||||
* "count": 1
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "ogs-core.h"
|
||||
#include "ogs-proto.h"
|
||||
#include "context.h"
|
||||
#include "gnb-info.h"
|
||||
|
||||
#include "sbi/openapi/external/cJSON.h"
|
||||
#include "metrics/prometheus/json_pager.h"
|
||||
|
||||
#ifndef GNB_INFO_PAGE_SIZE_DEFAULT
|
||||
#define GNB_INFO_PAGE_SIZE_DEFAULT 100U
|
||||
#endif
|
||||
|
||||
static size_t g_page = SIZE_MAX; /* SIZE_MAX => no paging */
|
||||
static size_t g_page_size = 0; /* 0 => use default in dumper */
|
||||
|
||||
void amf_metrics_gnb_info_set_pager(size_t page, size_t page_size)
|
||||
{
|
||||
g_page = page;
|
||||
g_page_size = page_size;
|
||||
}
|
||||
|
||||
size_t amf_dump_gnb_info(char *buf, size_t buflen)
|
||||
{
|
||||
return amf_dump_gnb_info_paged(buf, buflen, g_page, g_page_size);
|
||||
}
|
||||
|
||||
static inline uint32_t u24_to_u32(ogs_uint24_t v)
|
||||
{
|
||||
uint32_t x = 0; memcpy(&x, &v, sizeof(v) < sizeof(x) ? sizeof(v) : sizeof(x));
|
||||
return (x & 0xFFFFFFu);
|
||||
}
|
||||
|
||||
static inline const char *safe_sa_str(ogs_sockaddr_t *sa)
|
||||
{
|
||||
if (!sa) return "";
|
||||
int fam = sa->sa.sa_family;
|
||||
if (fam != AF_INET && fam != AF_INET6) return "";
|
||||
return ogs_sockaddr_to_string_static(sa);
|
||||
}
|
||||
|
||||
static inline int add_plmn_string(cJSON *obj, const char *key, const ogs_plmn_id_t *plmn)
|
||||
{
|
||||
char s[OGS_PLMNIDSTRLEN] = {0};
|
||||
if (plmn) ogs_plmn_id_to_string(plmn, s);
|
||||
return cJSON_AddStringToObject(obj, key, s) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ---------- main (paged) ---------- */
|
||||
|
||||
size_t amf_dump_gnb_info_paged(char *buf, size_t buflen, size_t page, size_t page_size)
|
||||
{
|
||||
if (!buf || buflen == 0) return 0;
|
||||
|
||||
const bool no_paging = (page == SIZE_MAX);
|
||||
if (!no_paging) {
|
||||
if (page_size == 0) page_size = GNB_INFO_PAGE_SIZE_DEFAULT;
|
||||
if (page_size > GNB_INFO_PAGE_SIZE_DEFAULT) page_size = GNB_INFO_PAGE_SIZE_DEFAULT;
|
||||
} else {
|
||||
page_size = SIZE_MAX;
|
||||
page = 0;
|
||||
}
|
||||
|
||||
amf_context_t *ctxt = amf_self();
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (!root) { if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; } buf[0] = '\0'; return 0; }
|
||||
|
||||
cJSON *items = cJSON_AddArrayToObject(root, "items");
|
||||
if (!items) { cJSON_Delete(root); if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; } buf[0] = '\0'; return 0; }
|
||||
|
||||
size_t idx = 0, emitted = 0;
|
||||
bool has_next = false, oom = false;
|
||||
|
||||
const size_t start_index = json_pager_safe_start_index(no_paging, page, page_size);
|
||||
|
||||
amf_gnb_t *gnb = NULL;
|
||||
ogs_list_for_each(&ctxt->gnb_list, gnb) {
|
||||
int act = json_pager_advance(no_paging, idx, start_index, emitted, page_size, &has_next);
|
||||
if (act == 1) { idx++; continue; }
|
||||
if (act == 2) break;
|
||||
|
||||
/* Build the item completely before attaching to items[] */
|
||||
cJSON *g = cJSON_CreateObject();
|
||||
if (!g) { oom = true; break; }
|
||||
|
||||
/* basics */
|
||||
if (!cJSON_AddNumberToObject(g, "gnb_id", (double)(unsigned)gnb->gnb_id)) { cJSON_Delete(g); oom = true; break; }
|
||||
if (add_plmn_string(g, "plmn", &gnb->plmn_id) < 0) { cJSON_Delete(g); oom = true; break; }
|
||||
|
||||
/* network */
|
||||
{
|
||||
cJSON *network = cJSON_CreateObject();
|
||||
if (!network) { cJSON_Delete(g); oom = true; break; }
|
||||
|
||||
if (!cJSON_AddStringToObject(network, "amf_name", ctxt->amf_name ? ctxt->amf_name : "")) { cJSON_Delete(network); cJSON_Delete(g); oom = true; break; }
|
||||
if (!cJSON_AddNumberToObject(network, "ngap_port", (double)(unsigned)ctxt->ngap_port)) { cJSON_Delete(network); cJSON_Delete(g); oom = true; break; }
|
||||
|
||||
cJSON_AddItemToObjectCS(g, "network", network);
|
||||
}
|
||||
|
||||
/* ng + sctp */
|
||||
{
|
||||
cJSON *ng = cJSON_CreateObject();
|
||||
if (!ng) { cJSON_Delete(g); oom = true; break; }
|
||||
|
||||
/* Build sctp first, then attach */
|
||||
cJSON *sctp = cJSON_CreateObject();
|
||||
if (!sctp) { cJSON_Delete(ng); cJSON_Delete(g); oom = true; break; }
|
||||
|
||||
if (!cJSON_AddStringToObject(sctp, "peer", safe_sa_str(gnb->sctp.addr))) { cJSON_Delete(sctp); cJSON_Delete(ng); cJSON_Delete(g); oom = true; break; }
|
||||
if (!cJSON_AddNumberToObject(sctp, "max_out_streams", (double)gnb->max_num_of_ostreams)) { cJSON_Delete(sctp); cJSON_Delete(ng); cJSON_Delete(g); oom = true; break; }
|
||||
if (!cJSON_AddNumberToObject(sctp, "next_ostream_id", (double)(unsigned)gnb->ostream_id)) { cJSON_Delete(sctp); cJSON_Delete(ng); cJSON_Delete(g); oom = true; break; }
|
||||
|
||||
cJSON_AddItemToObjectCS(ng, "sctp", sctp);
|
||||
|
||||
if (!cJSON_AddBoolToObject(ng, "setup_success", gnb->state.ng_setup_success ? 1 : 0)) { cJSON_Delete(ng); cJSON_Delete(g); oom = true; break; }
|
||||
|
||||
cJSON_AddItemToObjectCS(g, "ng", ng);
|
||||
}
|
||||
|
||||
/* supported_ta_list */
|
||||
{
|
||||
cJSON *tas = cJSON_CreateArray();
|
||||
if (!tas) { cJSON_Delete(g); oom = true; break; }
|
||||
|
||||
bool inner_oom = false;
|
||||
|
||||
for (int t = 0; t < gnb->num_of_supported_ta_list; t++) {
|
||||
const ogs_uint24_t tac = gnb->supported_ta_list[t].tac;
|
||||
const int nbp = gnb->supported_ta_list[t].num_of_bplmn_list;
|
||||
|
||||
cJSON *ta = cJSON_CreateObject();
|
||||
if (!ta) { inner_oom = true; break; }
|
||||
|
||||
char tac_hex[7]; snprintf(tac_hex, sizeof tac_hex, "%06x", (unsigned)u24_to_u32(tac));
|
||||
if (!cJSON_AddStringToObject(ta, "tac", tac_hex)) { cJSON_Delete(ta); inner_oom = true; break; }
|
||||
|
||||
cJSON *bplmns = cJSON_CreateArray();
|
||||
if (!bplmns) { cJSON_Delete(ta); inner_oom = true; break; }
|
||||
|
||||
bool inner2_oom = false;
|
||||
|
||||
for (int p = 0; p < nbp; p++) {
|
||||
const ogs_plmn_id_t *bp_plmn = &gnb->supported_ta_list[t].bplmn_list[p].plmn_id;
|
||||
const int ns = gnb->supported_ta_list[t].bplmn_list[p].num_of_s_nssai;
|
||||
const ogs_s_nssai_t *sn = gnb->supported_ta_list[t].bplmn_list[p].s_nssai;
|
||||
|
||||
cJSON *bp = cJSON_CreateObject();
|
||||
if (!bp) { inner2_oom = true; break; }
|
||||
|
||||
if (add_plmn_string(bp, "plmn", bp_plmn) < 0) { cJSON_Delete(bp); inner2_oom = true; break; }
|
||||
|
||||
cJSON *sns = cJSON_CreateArray();
|
||||
if (!sns) { cJSON_Delete(bp); inner2_oom = true; break; }
|
||||
|
||||
bool inner3_oom = false;
|
||||
|
||||
for (int i = 0; i < ns; i++) {
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (!o) { inner3_oom = true; break; }
|
||||
|
||||
if (!cJSON_AddNumberToObject(o, "sst", (double)(unsigned)sn[i].sst)) { cJSON_Delete(o); inner3_oom = true; break; }
|
||||
char sd[7]; snprintf(sd, sizeof sd, "%06x", (unsigned)u24_to_u32(sn[i].sd));
|
||||
if (!cJSON_AddStringToObject(o, "sd", sd)) { cJSON_Delete(o); inner3_oom = true; break; }
|
||||
|
||||
cJSON_AddItemToArray(sns, o);
|
||||
}
|
||||
|
||||
if (inner3_oom) { cJSON_Delete(sns); cJSON_Delete(bp); inner2_oom = true; break; }
|
||||
|
||||
cJSON_AddItemToObjectCS(bp, "snssai", sns);
|
||||
cJSON_AddItemToArray(bplmns, bp);
|
||||
}
|
||||
|
||||
if (inner2_oom) { cJSON_Delete(bplmns); cJSON_Delete(ta); inner_oom = true; break; }
|
||||
|
||||
cJSON_AddItemToObjectCS(ta, "bplmns", bplmns);
|
||||
cJSON_AddItemToArray(tas, ta);
|
||||
}
|
||||
|
||||
if (inner_oom) { cJSON_Delete(tas); cJSON_Delete(g); oom = true; break; }
|
||||
|
||||
cJSON_AddItemToObjectCS(g, "supported_ta_list", tas);
|
||||
}
|
||||
|
||||
/* num_connected_ues */
|
||||
{
|
||||
size_t num = 0; ran_ue_t *r = NULL; ogs_list_for_each(&((amf_gnb_t*)gnb)->ran_ue_list, r) num++;
|
||||
if (!cJSON_AddNumberToObject(g, "num_connected_ues", (double)num)) { cJSON_Delete(g); oom = true; break; }
|
||||
}
|
||||
|
||||
/* attach completed item */
|
||||
cJSON_AddItemToArray(items, g);
|
||||
emitted++;
|
||||
idx++;
|
||||
}
|
||||
|
||||
json_pager_add_trailing(root, no_paging, page, page_size, emitted, has_next && !oom,"/gnb-info", oom);
|
||||
|
||||
return json_pager_finalize(root, buf, buflen);
|
||||
}
|
||||
|
43
src/amf/gnb-info.h
Normal file
43
src/amf/gnb-info.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* /gnb-info — AMF-side JSON exporter (Prometheus HTTP endpoint)
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef GNB_INFO_PAGE_SIZE_DEFAULT
|
||||
#define GNB_INFO_PAGE_SIZE_DEFAULT 100U
|
||||
#endif
|
||||
|
||||
size_t amf_dump_gnb_info(char *buf, size_t buflen);
|
||||
size_t amf_dump_gnb_info_paged(char *buf, size_t buflen, size_t page, size_t page_size);
|
||||
void amf_metrics_gnb_info_set_pager(size_t page, size_t page_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
@@ -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;
|
||||
|
@@ -61,7 +61,9 @@ libamf_sources = files('''
|
||||
|
||||
init.c
|
||||
metrics.c
|
||||
connected_gnbs.c
|
||||
gnb-info.c
|
||||
ue-info.c
|
||||
../../lib/metrics/prometheus/json_pager.c
|
||||
'''.split())
|
||||
|
||||
libamf = static_library('amf',
|
||||
|
633
src/amf/ue-info.c
Normal file
633
src/amf/ue-info.c
Normal file
@@ -0,0 +1,633 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Connected AMF UEs JSON dumper for the Prometheus HTTP server (/ue-info).
|
||||
* - supi, suci, pei, cm_state, guti,m_tmsi, gnb_info, location, security, ambr, slices, am_policy
|
||||
* - pager: /ue-info?page=0&page_size=100 (0-based, page=-1 without paging) Default: page=0 page_size=100=MAXSIZE
|
||||
*
|
||||
* path: http://AMF_IP:9090/ue-info
|
||||
* curl -s "http://127.0.0.5:9090/ue-info" |jq .
|
||||
* {
|
||||
* "items": [
|
||||
* {
|
||||
* "supi": "imsi-001010000056492",
|
||||
* "suci": "suci-0-001-01-0000-1-1-85731c4241d466407311a85135e914987944f361550f13f4532e29bdc5859548a363cc1a81798bb4cc59f2b4db",
|
||||
* "pei": "imeisv-3535938300494715",
|
||||
* "cm_state": "connected",
|
||||
* "guti": "00101-030080-CC00007AB",
|
||||
* "m_tmsi": 3221227435,
|
||||
* "gnb": {
|
||||
* "ostream_id": 1,
|
||||
* "amf_ue_ngap_id": 2,
|
||||
* "ran_ue_ngap_id": 39,
|
||||
* "gnb_id": 100,
|
||||
* "cell_id": 1
|
||||
* },
|
||||
* "location": {
|
||||
* "timestamp": 1758733375895512,
|
||||
* "nr_tai": {
|
||||
* "plmn": "99970",
|
||||
* "tac_hex": "000001",
|
||||
* "tac": 1
|
||||
* },
|
||||
* "nr_cgi": {
|
||||
* "plmn": "99970",
|
||||
* "nci": 1638401,
|
||||
* "gnb_id": 100,
|
||||
* "cell_id": 1
|
||||
* },
|
||||
* "last_visited_plmn_id": "000000"
|
||||
* },
|
||||
* "msisdn": [],
|
||||
* "security": {
|
||||
* "valid": 1,
|
||||
* "enc": "nea2",
|
||||
* "int": "nia2"
|
||||
* },
|
||||
* "ambr": {
|
||||
* "downlink": 1000000000,
|
||||
* "uplink": 1000000000
|
||||
* },
|
||||
* "pdu_sessions": [],
|
||||
* "pdu_sessions_count": 0,
|
||||
* "requested_slices": [
|
||||
* {
|
||||
* "sst": 1,
|
||||
* "sd": "ffffff"
|
||||
* }
|
||||
* ],
|
||||
* "allowed_slices": [
|
||||
* {
|
||||
* "sst": 1,
|
||||
* "sd": "ffffff"
|
||||
* }
|
||||
* ],
|
||||
* "requested_slices_count": 1,
|
||||
* "allowed_slices_count": 1,
|
||||
* "am_policy_features": 4,
|
||||
* "am_policy_features_info": {
|
||||
* "hex": "0x0000000000000004",
|
||||
* "bits": [
|
||||
* 2
|
||||
* ],
|
||||
* "labels": [
|
||||
* "QoS Policy Control"
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* ],
|
||||
* "pager": {
|
||||
* "page": 0,
|
||||
* "page_size": 100,
|
||||
* "count": 2
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ogs-core.h"
|
||||
#include "ogs-proto.h"
|
||||
#include "context.h"
|
||||
#include "ue-info.h"
|
||||
|
||||
#include "sbi/openapi/external/cJSON.h"
|
||||
#include "metrics/prometheus/json_pager.h"
|
||||
|
||||
static size_t g_ue_page = 0;
|
||||
static size_t g_ue_page_size = 0;
|
||||
|
||||
void amf_metrics_ue_info_set_pager(size_t page, size_t page_size)
|
||||
{
|
||||
g_ue_page = page;
|
||||
g_ue_page_size = page_size;
|
||||
}
|
||||
|
||||
size_t amf_dump_ue_info(char *buf, size_t buflen)
|
||||
{
|
||||
size_t page = g_ue_page;
|
||||
size_t page_size = g_ue_page_size ? g_ue_page_size : 100;
|
||||
if (page_size > 100) page_size = 100;
|
||||
return amf_dump_ue_info_paged(buf, buflen, page, page_size);
|
||||
}
|
||||
|
||||
static inline uint32_t u24_to_u32(ogs_uint24_t v)
|
||||
{
|
||||
uint32_t x = 0;
|
||||
memcpy(&x, &v, sizeof(v) < sizeof(x) ? sizeof(v) : sizeof(x));
|
||||
return (x & 0xFFFFFFu);
|
||||
}
|
||||
|
||||
static inline void bytes3_to_hex_lower(const void *p, char out[7])
|
||||
{
|
||||
const uint8_t *b = (const uint8_t *)p;
|
||||
static const char *hx = "0123456789abcdef";
|
||||
out[0] = hx[(b[0] >> 4) & 0xF];
|
||||
out[1] = hx[b[0] & 0xF];
|
||||
out[2] = hx[(b[1] >> 4) & 0xF];
|
||||
out[3] = hx[b[1] & 0xF];
|
||||
out[4] = hx[(b[2] >> 4) & 0xF];
|
||||
out[5] = hx[b[2] & 0xF];
|
||||
out[6] = '\0';
|
||||
}
|
||||
|
||||
/* AM policy feature labels */
|
||||
static const char *am_policy_feature_names[64] = {
|
||||
/*0*/ "AM Policy Association",
|
||||
/*1*/ "Slice-specific Policy",
|
||||
/*2*/ "QoS Policy Control",
|
||||
/*3*/ "Access & Mobility Policy",
|
||||
/*4*/ "Charging Policy",
|
||||
/*5*/ "Traffic Steering",
|
||||
/*6*/ "Roaming Policy",
|
||||
/*7*/ "Emergency Services Policy",
|
||||
/*8*/ "UE Policy",
|
||||
/*9*/ "Session Continuity Policy",
|
||||
/*10*/ "Application-based Policy",
|
||||
/*11*/ "Location-based Policy",
|
||||
/*12*/ "Time-based Policy",
|
||||
/*13*/ "Data Network Selection",
|
||||
/*14*/ "Policy Update Notification",
|
||||
/*15*/ "Policy Control Event Exposure",
|
||||
};
|
||||
|
||||
/* -------- JSON builders: 0=OK, -1=OOM. Children are attached only when complete. -------- */
|
||||
static int add_snssai_sd_string(cJSON *obj, const char *key, const void *sd_ptr)
|
||||
{
|
||||
char sd_hex[7] = "ffffff";
|
||||
if (sd_ptr) bytes3_to_hex_lower(sd_ptr, sd_hex);
|
||||
cJSON *s = cJSON_CreateString(sd_hex);
|
||||
if (!s) return -1;
|
||||
cJSON_AddItemToObjectCS(obj, key, s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_basic_identity(cJSON *o, const amf_ue_t *ue)
|
||||
{
|
||||
if (ue->supi && ue->supi[0]) {
|
||||
cJSON *v = cJSON_CreateString(ue->supi); if (!v) return -1;
|
||||
cJSON_AddItemToObjectCS(o, "supi", v);
|
||||
}
|
||||
if (ue->suci && ue->suci[0]) {
|
||||
cJSON *v = cJSON_CreateString(ue->suci); if (!v) return -1;
|
||||
cJSON_AddItemToObjectCS(o, "suci", v);
|
||||
}
|
||||
if (ue->pei && ue->pei[0]) {
|
||||
cJSON *v = cJSON_CreateString(ue->pei); if (!v) return -1;
|
||||
cJSON_AddItemToObjectCS(o, "pei", v);
|
||||
}
|
||||
|
||||
{
|
||||
const char *cm = CM_CONNECTED(ue) ? "connected" : "idle";
|
||||
cJSON *v = cJSON_CreateString(cm); if (!v) return -1;
|
||||
cJSON_AddItemToObjectCS(o, "cm_state", v);
|
||||
}
|
||||
|
||||
/* If M-TMSI present, expose GUTI string and m_tmsi numeric */
|
||||
if (ue->current.m_tmsi) {
|
||||
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
|
||||
ogs_plmn_id_to_string(&ue->guami->plmn_id, plmn_str);
|
||||
|
||||
/* ogs_amf_id_to_string() returns heap memory — free it */
|
||||
char *amf_hex = ogs_amf_id_to_string(&ue->guami->amf_id);
|
||||
const char *amf_str = amf_hex ? amf_hex : "";
|
||||
|
||||
char guti_buf[80];
|
||||
(void)snprintf(guti_buf, sizeof guti_buf, "%s-%s-C%08X",
|
||||
plmn_str, amf_str, (unsigned)ue->current.guti.m_tmsi);
|
||||
|
||||
cJSON *g = cJSON_CreateString(guti_buf);
|
||||
if (amf_hex) ogs_free(amf_hex);
|
||||
if (!g) return -1;
|
||||
cJSON_AddItemToObjectCS(o, "guti", g);
|
||||
|
||||
cJSON *m = cJSON_CreateNumber((double)ue->current.guti.m_tmsi);
|
||||
if (!m) return -1;
|
||||
cJSON_AddItemToObjectCS(o, "m_tmsi", m);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_gnb(cJSON *parent, const amf_ue_t *ue)
|
||||
{
|
||||
cJSON *gnb = cJSON_CreateObject();
|
||||
if (!gnb) return -1;
|
||||
|
||||
cJSON *osid = cJSON_CreateNumber((double)ue->gnb_ostream_id);
|
||||
if (!osid) { cJSON_Delete(gnb); return -1; }
|
||||
cJSON_AddItemToObjectCS(gnb, "ostream_id", osid);
|
||||
|
||||
ran_ue_t *ran = ran_ue_find_by_id(ue->ran_ue_id);
|
||||
if (ran) {
|
||||
uint64_t nci = ue->nr_cgi.cell_id & 0xFFFFFFFFFULL; /* 36-bit */
|
||||
uint32_t gnb_id = (uint32_t)((nci >> 14) & 0x3FFFFF);
|
||||
uint32_t cell_id = (uint32_t)(nci & 0x3FFF);
|
||||
|
||||
cJSON *a = cJSON_CreateNumber((double)ran->amf_ue_ngap_id);
|
||||
cJSON *r = cJSON_CreateNumber((double)ran->ran_ue_ngap_id);
|
||||
cJSON *g = cJSON_CreateNumber((double)gnb_id);
|
||||
cJSON *c = cJSON_CreateNumber((double)cell_id);
|
||||
if (!a || !r || !g || !c) {
|
||||
if (a) cJSON_Delete(a);
|
||||
if (r) cJSON_Delete(r);
|
||||
if (g) cJSON_Delete(g);
|
||||
if (c) cJSON_Delete(c);
|
||||
cJSON_Delete(gnb);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddItemToObjectCS(gnb, "amf_ue_ngap_id", a);
|
||||
cJSON_AddItemToObjectCS(gnb, "ran_ue_ngap_id", r);
|
||||
cJSON_AddItemToObjectCS(gnb, "gnb_id", g);
|
||||
cJSON_AddItemToObjectCS(gnb, "cell_id", c);
|
||||
} else {
|
||||
cJSON *st = cJSON_CreateString("not-connected");
|
||||
if (!st) { cJSON_Delete(gnb); return -1; }
|
||||
cJSON_AddItemToObjectCS(gnb, "status", st);
|
||||
}
|
||||
|
||||
cJSON_AddItemToObjectCS(parent, "gnb", gnb);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_location(cJSON *parent, const amf_ue_t *ue)
|
||||
{
|
||||
cJSON *loc = cJSON_CreateObject();
|
||||
if (!loc) return -1;
|
||||
|
||||
cJSON *ts = cJSON_CreateNumber((double)ue->ue_location_timestamp);
|
||||
if (!ts) { cJSON_Delete(loc); return -1; }
|
||||
cJSON_AddItemToObjectCS(loc, "timestamp", ts);
|
||||
|
||||
/* nr_tai */
|
||||
{
|
||||
cJSON *tai = cJSON_CreateObject();
|
||||
if (!tai) { cJSON_Delete(loc); return -1; }
|
||||
|
||||
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
|
||||
ogs_plmn_id_to_string(&ue->nr_tai.plmn_id, plmn_str);
|
||||
cJSON *p = cJSON_CreateString(plmn_str);
|
||||
if (!p) { cJSON_Delete(tai); cJSON_Delete(loc); return -1; }
|
||||
cJSON_AddItemToObjectCS(tai, "plmn", p);
|
||||
|
||||
char tac_hex[7];
|
||||
(void)snprintf(tac_hex, sizeof tac_hex, "%06x", (unsigned)u24_to_u32(ue->nr_tai.tac));
|
||||
cJSON *th = cJSON_CreateString(tac_hex);
|
||||
cJSON *tn = cJSON_CreateNumber((double)u24_to_u32(ue->nr_tai.tac));
|
||||
if (!th || !tn) { if (th) cJSON_Delete(th); if (tn) cJSON_Delete(tn); cJSON_Delete(tai); cJSON_Delete(loc); return -1; }
|
||||
cJSON_AddItemToObjectCS(tai, "tac_hex", th);
|
||||
cJSON_AddItemToObjectCS(tai, "tac", tn);
|
||||
|
||||
cJSON_AddItemToObjectCS(loc, "nr_tai", tai);
|
||||
}
|
||||
|
||||
/* nr_cgi */
|
||||
{
|
||||
cJSON *cgi = cJSON_CreateObject();
|
||||
if (!cgi) { cJSON_Delete(loc); return -1; }
|
||||
|
||||
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
|
||||
ogs_plmn_id_to_string(&ue->nr_cgi.plmn_id, plmn_str);
|
||||
cJSON *p = cJSON_CreateString(plmn_str);
|
||||
if (!p) { cJSON_Delete(cgi); cJSON_Delete(loc); return -1; }
|
||||
cJSON_AddItemToObjectCS(cgi, "plmn", p);
|
||||
|
||||
uint64_t nci = ue->nr_cgi.cell_id & 0xFFFFFFFFFULL; /* 36-bit */
|
||||
uint32_t gnb_id = (uint32_t)((nci >> 14) & 0x3FFFFF);
|
||||
uint32_t cell_id = (uint32_t)(nci & 0x3FFF);
|
||||
|
||||
cJSON *n = cJSON_CreateNumber((double)nci);
|
||||
cJSON *g = cJSON_CreateNumber((double)gnb_id);
|
||||
cJSON *c = cJSON_CreateNumber((double)cell_id);
|
||||
if (!n || !g || !c) { if (n) cJSON_Delete(n); if (g) cJSON_Delete(g); if (c) cJSON_Delete(c); cJSON_Delete(cgi); cJSON_Delete(loc); return -1; }
|
||||
cJSON_AddItemToObjectCS(cgi, "nci", n);
|
||||
cJSON_AddItemToObjectCS(cgi, "gnb_id", g);
|
||||
cJSON_AddItemToObjectCS(cgi, "cell_id", c);
|
||||
|
||||
cJSON_AddItemToObjectCS(loc, "nr_cgi", cgi);
|
||||
}
|
||||
|
||||
/* last_visited_plmn_id */
|
||||
{
|
||||
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
|
||||
ogs_plmn_id_to_string(&ue->last_visited_plmn_id, plmn_str);
|
||||
cJSON *lv = cJSON_CreateString(plmn_str);
|
||||
if (!lv) { cJSON_Delete(loc); return -1; }
|
||||
cJSON_AddItemToObjectCS(loc, "last_visited_plmn_id", lv);
|
||||
}
|
||||
|
||||
cJSON_AddItemToObjectCS(parent, "location", loc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_msisdn_array(cJSON *parent, const amf_ue_t *ue)
|
||||
{
|
||||
cJSON *arr = cJSON_CreateArray();
|
||||
if (!arr) return -1;
|
||||
|
||||
for (int i = 0; i < ue->num_of_msisdn; i++) {
|
||||
if (!ue->msisdn[i] || !ue->msisdn[i][0]) continue;
|
||||
cJSON *s = cJSON_CreateString(ue->msisdn[i]);
|
||||
if (!s) { cJSON_Delete(arr); return -1; }
|
||||
cJSON_AddItemToArray(arr, s);
|
||||
}
|
||||
|
||||
cJSON_AddItemToObjectCS(parent, "msisdn", arr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_security(cJSON *parent, const amf_ue_t *ue)
|
||||
{
|
||||
cJSON *sec = cJSON_CreateObject();
|
||||
if (!sec) return -1;
|
||||
|
||||
{
|
||||
int valid_ctx = SECURITY_CONTEXT_IS_VALID(ue);
|
||||
cJSON *v = cJSON_CreateNumber((double)valid_ctx);
|
||||
if (!v) { cJSON_Delete(sec); return -1; }
|
||||
cJSON_AddItemToObjectCS(sec, "valid", v);
|
||||
}
|
||||
|
||||
const char *enc =
|
||||
ue->selected_enc_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NEA2 ? "nea2" :
|
||||
ue->selected_enc_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NEA1 ? "nea1" :
|
||||
ue->selected_enc_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NEA3 ? "nea3" : "nea0";
|
||||
|
||||
const char *integ =
|
||||
ue->selected_int_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NIA2 ? "nia2" :
|
||||
ue->selected_int_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NIA1 ? "nia1" :
|
||||
ue->selected_int_algorithm == OGS_NAS_SECURITY_ALGORITHMS_128_NIA3 ? "nia3" : "nia0";
|
||||
|
||||
cJSON *e = cJSON_CreateString(enc);
|
||||
cJSON *i = cJSON_CreateString(integ);
|
||||
if (!e || !i) { if (e) cJSON_Delete(e); if (i) cJSON_Delete(i); cJSON_Delete(sec); return -1; }
|
||||
cJSON_AddItemToObjectCS(sec, "enc", e);
|
||||
cJSON_AddItemToObjectCS(sec, "int", i);
|
||||
|
||||
cJSON_AddItemToObjectCS(parent, "security", sec);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_ambr(cJSON *parent, const amf_ue_t *ue)
|
||||
{
|
||||
cJSON *ambr = cJSON_CreateObject();
|
||||
if (!ambr) return -1;
|
||||
|
||||
cJSON *dl = cJSON_CreateNumber((double)ue->ue_ambr.downlink);
|
||||
cJSON *ul = cJSON_CreateNumber((double)ue->ue_ambr.uplink);
|
||||
if (!dl || !ul) { if (dl) cJSON_Delete(dl); if (ul) cJSON_Delete(ul); cJSON_Delete(ambr); return -1; }
|
||||
|
||||
cJSON_AddItemToObjectCS(ambr, "downlink", dl);
|
||||
cJSON_AddItemToObjectCS(ambr, "uplink", ul);
|
||||
|
||||
cJSON_AddItemToObjectCS(parent, "ambr", ambr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_pdu_sessions(cJSON *parent, const amf_ue_t *ue)
|
||||
{
|
||||
cJSON *arr = cJSON_CreateArray();
|
||||
if (!arr) return -1;
|
||||
|
||||
int count = 0;
|
||||
amf_sess_t *sess = NULL;
|
||||
ogs_list_for_each(&(ue->sess_list), sess) {
|
||||
cJSON *it = cJSON_CreateObject();
|
||||
if (!it) { cJSON_Delete(arr); return -1; }
|
||||
|
||||
/* PSI */
|
||||
{
|
||||
cJSON *n = cJSON_CreateNumber((double)sess->psi);
|
||||
if (!n) { cJSON_Delete(it); cJSON_Delete(arr); return -1; }
|
||||
cJSON_AddItemToObjectCS(it, "psi", n);
|
||||
}
|
||||
|
||||
/* DNN */
|
||||
if (sess->dnn && sess->dnn[0]) {
|
||||
cJSON *d = cJSON_CreateString(sess->dnn);
|
||||
if (!d) { cJSON_Delete(it); cJSON_Delete(arr); return -1; }
|
||||
cJSON_AddItemToObjectCS(it, "dnn", d);
|
||||
}
|
||||
|
||||
/* S-NSSAI */
|
||||
{
|
||||
cJSON *sn = cJSON_CreateObject();
|
||||
if (!sn) { cJSON_Delete(it); cJSON_Delete(arr); return -1; }
|
||||
cJSON *sst = cJSON_CreateNumber((double)sess->s_nssai.sst);
|
||||
if (!sst) { cJSON_Delete(sn); cJSON_Delete(it); cJSON_Delete(arr); return -1; }
|
||||
cJSON_AddItemToObjectCS(sn, "sst", sst);
|
||||
if (add_snssai_sd_string(sn, "sd", &sess->s_nssai.sd) < 0) { cJSON_Delete(sn); cJSON_Delete(it); cJSON_Delete(arr); return -1; }
|
||||
cJSON_AddItemToObjectCS(it, "snssai", sn);
|
||||
}
|
||||
|
||||
/* Flags/counters mirrored as-is */
|
||||
{
|
||||
cJSON *lbo = cJSON_CreateBool(sess->lbo_roaming_allowed);
|
||||
cJSON *rs = cJSON_CreateNumber((double)sess->resource_status);
|
||||
cJSON *n1 = cJSON_CreateBool(sess->n1_released);
|
||||
cJSON *n2 = cJSON_CreateBool(sess->n2_released);
|
||||
if (!lbo || !rs || !n1 || !n2) {
|
||||
if (lbo) cJSON_Delete(lbo);
|
||||
if (rs) cJSON_Delete(rs);
|
||||
if (n1) cJSON_Delete(n1);
|
||||
if (n2) cJSON_Delete(n2);
|
||||
cJSON_Delete(it);
|
||||
cJSON_Delete(arr);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddItemToObjectCS(it, "lbo_roaming_allowed", lbo);
|
||||
cJSON_AddItemToObjectCS(it, "resource_status", rs);
|
||||
cJSON_AddItemToObjectCS(it, "n1_released", n1);
|
||||
cJSON_AddItemToObjectCS(it, "n2_released", n2);
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(arr, it);
|
||||
count++;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObjectCS(parent, "pdu_sessions", arr);
|
||||
{
|
||||
cJSON *n = cJSON_CreateNumber((double)count);
|
||||
if (!n) return -1;
|
||||
cJSON_AddItemToObjectCS(parent, "pdu_sessions_count", n);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_requested_allowed_slices(cJSON *parent, const amf_ue_t *ue)
|
||||
{
|
||||
cJSON *req = cJSON_CreateArray();
|
||||
cJSON *allow = cJSON_CreateArray();
|
||||
if (!req || !allow) { if (req) cJSON_Delete(req); if (allow) cJSON_Delete(allow); return -1; }
|
||||
|
||||
/* requested */
|
||||
for (int i = 0; i < ue->requested_nssai.num_of_s_nssai; i++) {
|
||||
const ogs_nas_s_nssai_ie_t *ie = &ue->requested_nssai.s_nssai[i];
|
||||
cJSON *sn = cJSON_CreateObject();
|
||||
if (!sn) { cJSON_Delete(req); cJSON_Delete(allow); return -1; }
|
||||
cJSON *sst = cJSON_CreateNumber((double)ie->sst);
|
||||
if (!sst) { cJSON_Delete(sn); cJSON_Delete(req); cJSON_Delete(allow); return -1; }
|
||||
cJSON_AddItemToObjectCS(sn, "sst", sst);
|
||||
if (add_snssai_sd_string(sn, "sd", &ie->sd) < 0) { cJSON_Delete(sn); cJSON_Delete(req); cJSON_Delete(allow); return -1; }
|
||||
cJSON_AddItemToArray(req, sn);
|
||||
}
|
||||
|
||||
/* allowed */
|
||||
for (int i = 0; i < ue->allowed_nssai.num_of_s_nssai; i++) {
|
||||
const ogs_nas_s_nssai_ie_t *ie = &ue->allowed_nssai.s_nssai[i];
|
||||
cJSON *sn = cJSON_CreateObject();
|
||||
if (!sn) { cJSON_Delete(req); cJSON_Delete(allow); return -1; }
|
||||
cJSON *sst = cJSON_CreateNumber((double)ie->sst);
|
||||
if (!sst) { cJSON_Delete(sn); cJSON_Delete(req); cJSON_Delete(allow); return -1; }
|
||||
cJSON_AddItemToObjectCS(sn, "sst", sst);
|
||||
if (add_snssai_sd_string(sn, "sd", &ie->sd) < 0) { cJSON_Delete(sn); cJSON_Delete(req); cJSON_Delete(allow); return -1; }
|
||||
cJSON_AddItemToArray(allow, sn);
|
||||
}
|
||||
|
||||
cJSON_AddItemToObjectCS(parent, "requested_slices", req);
|
||||
cJSON_AddItemToObjectCS(parent, "allowed_slices", allow);
|
||||
|
||||
cJSON *rc = cJSON_CreateNumber((double)ue->requested_nssai.num_of_s_nssai);
|
||||
cJSON *ac = cJSON_CreateNumber((double)ue->allowed_nssai.num_of_s_nssai);
|
||||
if (!rc || !ac) { if (rc) cJSON_Delete(rc); if (ac) cJSON_Delete(ac); return -1; }
|
||||
cJSON_AddItemToObjectCS(parent, "requested_slices_count", rc);
|
||||
cJSON_AddItemToObjectCS(parent, "allowed_slices_count", ac);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_am_policy_features(cJSON *parent, const amf_ue_t *ue)
|
||||
{
|
||||
uint64_t f = ue->am_policy_control_features;
|
||||
|
||||
cJSON *fv = cJSON_CreateNumber((double)f);
|
||||
if (!fv) return -1;
|
||||
cJSON_AddItemToObjectCS(parent, "am_policy_features", fv);
|
||||
|
||||
cJSON *feat = cJSON_CreateObject();
|
||||
if (!feat) return -1;
|
||||
|
||||
char hex[2 + 16 + 1];
|
||||
(void)snprintf(hex, sizeof hex, "0x%016" PRIx64, f);
|
||||
cJSON *hx = cJSON_CreateString(hex);
|
||||
if (!hx) { cJSON_Delete(feat); return -1; }
|
||||
cJSON_AddItemToObjectCS(feat, "hex", hx);
|
||||
|
||||
cJSON *bits = cJSON_CreateArray();
|
||||
cJSON *labels = cJSON_CreateArray();
|
||||
if (!bits || !labels) { if (bits) cJSON_Delete(bits); if (labels) cJSON_Delete(labels); cJSON_Delete(feat); return -1; }
|
||||
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if ((f >> i) & 1ULL) {
|
||||
cJSON *bi = cJSON_CreateNumber((double)i);
|
||||
if (!bi) { cJSON_Delete(bits); cJSON_Delete(labels); cJSON_Delete(feat); return -1; }
|
||||
cJSON_AddItemToArray(bits, bi);
|
||||
|
||||
const char *label = (i < 64 && am_policy_feature_names[i] && am_policy_feature_names[i][0])
|
||||
? am_policy_feature_names[i] : NULL;
|
||||
char fb[16];
|
||||
if (!label) { (void)snprintf(fb, sizeof fb, "bit%d", i); label = fb; }
|
||||
|
||||
cJSON *ls = cJSON_CreateString(label);
|
||||
if (!ls) { cJSON_Delete(bits); cJSON_Delete(labels); cJSON_Delete(feat); return -1; }
|
||||
cJSON_AddItemToArray(labels, ls);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_AddItemToObjectCS(feat, "bits", bits);
|
||||
cJSON_AddItemToObjectCS(feat, "labels", labels);
|
||||
cJSON_AddItemToObjectCS(parent, "am_policy_features_info", feat);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static cJSON *amf_ue_to_json(const amf_ue_t *ue)
|
||||
{
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (!o) return NULL;
|
||||
|
||||
if (add_basic_identity(o, ue) < 0) goto fail;
|
||||
if (add_gnb(o, ue) < 0) goto fail;
|
||||
if (add_location(o, ue) < 0) goto fail;
|
||||
if (add_msisdn_array(o, ue) < 0) goto fail;
|
||||
if (add_security(o, ue) < 0) goto fail;
|
||||
if (add_ambr(o, ue) < 0) goto fail;
|
||||
if (add_pdu_sessions(o, ue) < 0) goto fail;
|
||||
if (add_requested_allowed_slices(o, ue) < 0) goto fail;
|
||||
if (add_am_policy_features(o, ue) < 0) goto fail;
|
||||
|
||||
return o;
|
||||
|
||||
fail:
|
||||
cJSON_Delete(o);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t amf_dump_ue_info_paged(char *buf, size_t buflen, size_t page, size_t page_size)
|
||||
{
|
||||
if (!buf || buflen == 0) return 0;
|
||||
|
||||
const bool no_paging = (page == SIZE_MAX);
|
||||
if (!no_paging) {
|
||||
if (page_size == 0) page_size = 100;
|
||||
if (page_size > 100) page_size = 100;
|
||||
} else {
|
||||
page_size = SIZE_MAX;
|
||||
page = 0;
|
||||
}
|
||||
|
||||
const size_t start_index = json_pager_safe_start_index(no_paging, page, page_size);
|
||||
|
||||
amf_context_t *ctxt = amf_self();
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (!root) { if (buflen) buf[0] = '\0'; return 0; }
|
||||
|
||||
cJSON *items = cJSON_CreateArray();
|
||||
if (!items) { cJSON_Delete(root); if (buflen) buf[0] = '\0'; return 0; }
|
||||
cJSON_AddItemToObjectCS(root, "items", items);
|
||||
|
||||
size_t idx = 0, emitted = 0;
|
||||
bool has_next = false;
|
||||
bool oom = false;
|
||||
|
||||
amf_ue_t *ue = NULL;
|
||||
ogs_list_for_each(&ctxt->amf_ue_list, ue) {
|
||||
int act = json_pager_advance(no_paging, idx, start_index, emitted, page_size, &has_next);
|
||||
if (act == 1) { idx++; continue; }
|
||||
if (act == 2) break;
|
||||
|
||||
cJSON *one = amf_ue_to_json(ue);
|
||||
if (!one) { oom = true; break; }
|
||||
|
||||
cJSON_AddItemToArray(items, one);
|
||||
emitted++;
|
||||
idx++;
|
||||
}
|
||||
|
||||
/* add trailing pager info (json_pager_finalize will free 'root') */
|
||||
json_pager_add_trailing(root, no_paging, page, page_size, emitted,
|
||||
has_next && !oom, "/ue-info", oom);
|
||||
|
||||
return json_pager_finalize(root, buf, buflen);
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
@@ -1,190 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* JSON dumper for /connected-enbs (MME)
|
||||
* Output (one item per connected eNB):
|
||||
*[
|
||||
* {
|
||||
* "enb_id": 264040,
|
||||
* "plmn": "99970",
|
||||
* "network": {
|
||||
* "mme_name": "efire-mme0"
|
||||
* },
|
||||
* "s1": {
|
||||
* "setup_success": true,
|
||||
* "sctp": {
|
||||
* "peer": "[192.168.168.254]:36412",
|
||||
* "max_out_streams": 10,
|
||||
* "next_ostream_id": 1
|
||||
* }
|
||||
* },
|
||||
* "supported_ta_list": [
|
||||
* {
|
||||
* "tac": "000001",
|
||||
* "plmn": "99970"
|
||||
* }
|
||||
* ],
|
||||
* "num_connected_ues": 1
|
||||
* }
|
||||
*]
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "ogs-core.h"
|
||||
#include "ogs-proto.h"
|
||||
#include "ogs-app.h"
|
||||
#include "mme-context.h"
|
||||
|
||||
/* Exported */
|
||||
size_t mme_dump_connected_enbs(char *buf, size_t buflen);
|
||||
|
||||
/* ------------------------- small helpers ------------------------- */
|
||||
|
||||
static inline size_t append_safe(char *buf, size_t off, size_t buflen, const char *fmt, ...)
|
||||
{
|
||||
if (!buf || off == (size_t)-1 || off >= buflen) return (size_t)-1;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
|
||||
int n = ogs_vsnprintf(buf + off, buflen - off, fmt, ap);
|
||||
#pragma GCC diagnostic pop
|
||||
va_end(ap);
|
||||
if (n < 0 || (size_t)n >= buflen - off) return (size_t)-1;
|
||||
return off + (size_t)n;
|
||||
}
|
||||
|
||||
static size_t append_json_kv_escaped(char *buf, size_t off, size_t buflen,
|
||||
const char *key, const char *val)
|
||||
{
|
||||
if (!val) val = "";
|
||||
off = append_safe(buf, off, buflen, "\"%s\":\"", key);
|
||||
if (off == (size_t)-1) return off;
|
||||
for (const unsigned char *p = (const unsigned char *)val; *p; ++p) {
|
||||
unsigned char c = *p;
|
||||
if (c == '\\' || c == '\"') off = append_safe(buf, off, buflen, "\\%c", c);
|
||||
else if (c < 0x20) off = append_safe(buf, off, buflen, "\\u%04x", (unsigned)c);
|
||||
else off = append_safe(buf, off, buflen, "%c", c);
|
||||
if (off == (size_t)-1) return off;
|
||||
}
|
||||
return append_safe(buf, off, buflen, "\"");
|
||||
}
|
||||
|
||||
/* "plmn":"XXXXX" */
|
||||
static size_t append_plmn_kv(char *buf, size_t off, size_t buflen, const ogs_plmn_id_t *plmn)
|
||||
{
|
||||
char s[OGS_PLMNIDSTRLEN] = {0};
|
||||
ogs_plmn_id_to_string(plmn, s);
|
||||
return append_safe(buf, off, buflen, "\"plmn\":\"%s\"", s);
|
||||
}
|
||||
|
||||
static size_t append_u24_hex6_str(char *buf, size_t off, size_t buflen, uint32_t v24)
|
||||
{
|
||||
return append_safe(buf, off, buflen, "\"%06X\"", (unsigned)(v24 & 0xFFFFFF));
|
||||
}
|
||||
|
||||
static inline const char *safe_sa_str(const ogs_sockaddr_t *sa)
|
||||
{
|
||||
if (!sa) return "";
|
||||
int fam = ((const struct sockaddr *)&sa->sa)->sa_family;
|
||||
if (fam != AF_INET && fam != AF_INET6) return "";
|
||||
return ogs_sockaddr_to_string_static((ogs_sockaddr_t *)sa);
|
||||
}
|
||||
|
||||
#define APPF(...) do { off = append_safe(buf, off, buflen, __VA_ARGS__); if (off==(size_t)-1) goto trunc; } while(0)
|
||||
#define APPX(expr) do { off = (expr); if (off==(size_t)-1) goto trunc; } while(0)
|
||||
|
||||
/* ------------------------------- main ------------------------------- */
|
||||
|
||||
size_t mme_dump_connected_enbs(char *buf, size_t buflen)
|
||||
{
|
||||
size_t off = 0;
|
||||
if (!buf || buflen == 0) return 0;
|
||||
|
||||
mme_context_t *ctxt = mme_self();
|
||||
if (!ctxt) {
|
||||
APPF("[]");
|
||||
return off;
|
||||
}
|
||||
|
||||
APPF("[");
|
||||
bool first = true;
|
||||
|
||||
mme_enb_t *enb = NULL;
|
||||
ogs_list_for_each(&ctxt->enb_list, enb) {
|
||||
if (!first) APPF(",");
|
||||
first = false;
|
||||
|
||||
size_t num_connected_ues = 0;
|
||||
{
|
||||
enb_ue_t *ue = NULL;
|
||||
ogs_list_for_each(&enb->enb_ue_list, ue) num_connected_ues++;
|
||||
}
|
||||
|
||||
APPF("{");
|
||||
|
||||
APPF("\"enb_id\":%u", (unsigned)enb->enb_id);
|
||||
APPF(",");
|
||||
APPX(append_plmn_kv(buf, off, buflen, &enb->plmn_id));
|
||||
|
||||
APPF(",\"network\":{");
|
||||
APPX(append_json_kv_escaped(buf, off, buflen, "mme_name",
|
||||
ctxt->mme_name ? ctxt->mme_name : ""));
|
||||
APPF("}");
|
||||
|
||||
APPF(",\"s1\":{");
|
||||
APPF("\"setup_success\":%s", enb->state.s1_setup_success ? "true" : "false");
|
||||
APPF(",\"sctp\":{");
|
||||
APPF("\"peer\":\"%s\"", safe_sa_str(enb->sctp.addr));
|
||||
APPF(",\"max_out_streams\":%d", enb->max_num_of_ostreams);
|
||||
APPF(",\"next_ostream_id\":%u", (unsigned)enb->ostream_id);
|
||||
APPF("}");
|
||||
APPF("}");
|
||||
|
||||
APPF(",\"supported_ta_list\":[");
|
||||
for (int t = 0; t < enb->num_of_supported_ta_list; t++) {
|
||||
if (t) APPF(",");
|
||||
APPF("{");
|
||||
APPF("\"tac\":");
|
||||
APPX(append_u24_hex6_str(buf, off, buflen, enb->supported_ta_list[t].tac));
|
||||
APPF(",\"plmn\":");
|
||||
APPX(append_plmn_kv(buf, off, buflen, &enb->supported_ta_list[t].plmn_id));
|
||||
APPF("}");
|
||||
}
|
||||
APPF("]");
|
||||
|
||||
APPF(",\"num_connected_ues\":%zu", num_connected_ues);
|
||||
|
||||
APPF("}");
|
||||
}
|
||||
|
||||
APPF("]");
|
||||
return off;
|
||||
|
||||
trunc:
|
||||
if (buf && buflen >= 3) { buf[0]='['; buf[1]=']'; buf[2]='\0'; return 2; }
|
||||
if (buf && buflen) buf[0]='\0';
|
||||
return 0;
|
||||
}
|
||||
|
250
src/mme/enb-info.c
Normal file
250
src/mme/enb-info.c
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* ENB info - MME eNBs JSON dumper for the Prometheus HTTP server (/enb-info).
|
||||
* - enb_id, plmn, s1 info, enb_IP, supported ta_list, num_connceted_ues
|
||||
* - pager: /enb-info?page=0&page_size=100 (0-based, page=-1 without paging) Default: page=0 page_size=100=MAXSIZE
|
||||
*
|
||||
* curl -s "http://127.0.0.2:9090/enb-info?" |jq .
|
||||
* {
|
||||
* "items": [
|
||||
* {
|
||||
* "enb_id": 264040,
|
||||
* "plmn": "99970",
|
||||
* "network": {
|
||||
* "mme_name": "efire-mme0"
|
||||
* },
|
||||
* "s1": {
|
||||
* "sctp": {
|
||||
* "peer": "[192.168.168.254]:36412",
|
||||
* "max_out_streams": 10,
|
||||
* "next_ostream_id": 3
|
||||
* },
|
||||
* "setup_success": true
|
||||
* },
|
||||
* "supported_ta_list": [
|
||||
* {
|
||||
* "tac": "0001",
|
||||
* "plmn": "99970"
|
||||
* }
|
||||
* ],
|
||||
* "num_connected_ues": 1
|
||||
* }
|
||||
* ],
|
||||
* "pager": {
|
||||
* "page": 0,
|
||||
* "page_size": 100,
|
||||
* "count": 1
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "ogs-core.h"
|
||||
#include "ogs-proto.h"
|
||||
#include "mme-context.h"
|
||||
#include "enb-info.h"
|
||||
|
||||
#include "sbi/openapi/external/cJSON.h"
|
||||
#include "metrics/prometheus/json_pager.h"
|
||||
|
||||
#ifndef ENB_INFO_PAGE_SIZE_DEFAULT
|
||||
#define ENB_INFO_PAGE_SIZE_DEFAULT 100U
|
||||
#endif
|
||||
|
||||
static size_t g_enb_page = 0;
|
||||
static size_t g_enb_page_size = 0;
|
||||
|
||||
void mme_metrics_enb_info_set_pager(size_t page, size_t page_size)
|
||||
{
|
||||
g_enb_page = page;
|
||||
g_enb_page_size = page_size;
|
||||
}
|
||||
|
||||
size_t mme_dump_enb_info(char *buf, size_t buflen)
|
||||
{
|
||||
size_t page = g_enb_page;
|
||||
size_t page_size = g_enb_page_size ? g_enb_page_size : ENB_INFO_PAGE_SIZE_DEFAULT;
|
||||
if (page_size > ENB_INFO_PAGE_SIZE_DEFAULT) page_size = ENB_INFO_PAGE_SIZE_DEFAULT;
|
||||
return mme_dump_enb_info_paged(buf, buflen, page, page_size);
|
||||
}
|
||||
|
||||
static inline const char *safe_sa_str(const ogs_sockaddr_t *sa)
|
||||
{
|
||||
if (!sa) return "";
|
||||
int fam = ((const struct sockaddr *)&sa->sa)->sa_family;
|
||||
if (fam != AF_INET && fam != AF_INET6) return "";
|
||||
return ogs_sockaddr_to_string_static((ogs_sockaddr_t *)sa);
|
||||
}
|
||||
|
||||
size_t mme_dump_enb_info_paged(char *buf, size_t buflen, size_t page, size_t page_size)
|
||||
{
|
||||
if (!buf || buflen == 0) return 0;
|
||||
|
||||
const bool no_paging = (page == SIZE_MAX);
|
||||
if (!no_paging) {
|
||||
if (page_size == 0) page_size = ENB_INFO_PAGE_SIZE_DEFAULT;
|
||||
if (page_size > ENB_INFO_PAGE_SIZE_DEFAULT) page_size = ENB_INFO_PAGE_SIZE_DEFAULT;
|
||||
} else {
|
||||
page_size = SIZE_MAX;
|
||||
page = 0;
|
||||
}
|
||||
|
||||
const size_t start_index = json_pager_safe_start_index(no_paging, page, page_size);
|
||||
|
||||
mme_context_t *ctxt = mme_self();
|
||||
|
||||
/* root */
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (!root) {
|
||||
if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; }
|
||||
if (buflen) buf[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* items array */
|
||||
cJSON *items = cJSON_AddArrayToObject(root, "items");
|
||||
if (!items) {
|
||||
cJSON_Delete(root);
|
||||
if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; }
|
||||
if (buflen) buf[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t idx = 0, emitted = 0;
|
||||
bool has_next = false;
|
||||
bool oom = false;
|
||||
|
||||
mme_enb_t *enb = NULL;
|
||||
ogs_list_for_each(&ctxt->enb_list, enb) {
|
||||
int act = json_pager_advance(no_paging, idx, start_index, emitted, page_size, &has_next);
|
||||
if (act == 1) { idx++; continue; }
|
||||
if (act == 2) break;
|
||||
|
||||
/* Count connected UEs on this eNB */
|
||||
size_t num_connected_ues = 0;
|
||||
{
|
||||
enb_ue_t *ue_it = NULL;
|
||||
ogs_list_for_each(&enb->enb_ue_list, ue_it) num_connected_ues++;
|
||||
}
|
||||
|
||||
/* eNB object (build fully before attaching) */
|
||||
cJSON *e = cJSON_CreateObject();
|
||||
if (!e) { oom = true; break; }
|
||||
|
||||
/* enb_id */
|
||||
if (!cJSON_AddNumberToObject(e, "enb_id", (double)(unsigned)enb->enb_id)) { cJSON_Delete(e); oom = true; break; }
|
||||
|
||||
/* plmn */
|
||||
{
|
||||
char plmn_str[OGS_PLMNIDSTRLEN] = {0};
|
||||
ogs_plmn_id_to_string(&enb->plmn_id, plmn_str);
|
||||
if (!cJSON_AddStringToObject(e, "plmn", plmn_str)) { cJSON_Delete(e); oom = true; break; }
|
||||
}
|
||||
|
||||
/* network */
|
||||
{
|
||||
cJSON *network = cJSON_CreateObject();
|
||||
if (!network) { cJSON_Delete(e); oom = true; break; }
|
||||
if (!cJSON_AddStringToObject(network, "mme_name", ctxt->mme_name ? ctxt->mme_name : "")) {
|
||||
cJSON_Delete(network); cJSON_Delete(e); oom = true; break;
|
||||
}
|
||||
cJSON_AddItemToObjectCS(e, "network", network);
|
||||
}
|
||||
|
||||
/* s1 + sctp block */
|
||||
{
|
||||
cJSON *s1 = cJSON_CreateObject();
|
||||
if (!s1) { cJSON_Delete(e); oom = true; break; }
|
||||
|
||||
if (!cJSON_AddBoolToObject(s1, "setup_success", enb->state.s1_setup_success ? 1 : 0)) {
|
||||
cJSON_Delete(s1); cJSON_Delete(e); oom = true; break;
|
||||
}
|
||||
|
||||
/* sctp */
|
||||
cJSON *sctp = cJSON_CreateObject();
|
||||
if (!sctp) { cJSON_Delete(s1); cJSON_Delete(e); oom = true; break; }
|
||||
|
||||
if (!cJSON_AddStringToObject(sctp, "peer", safe_sa_str(enb->sctp.addr))) {
|
||||
cJSON_Delete(sctp); cJSON_Delete(s1); cJSON_Delete(e); oom = true; break;
|
||||
}
|
||||
if (!cJSON_AddNumberToObject(sctp, "max_out_streams", (double)enb->max_num_of_ostreams)) {
|
||||
cJSON_Delete(sctp); cJSON_Delete(s1); cJSON_Delete(e); oom = true; break;
|
||||
}
|
||||
if (!cJSON_AddNumberToObject(sctp, "next_ostream_id", (double)(unsigned)enb->ostream_id)) {
|
||||
cJSON_Delete(sctp); cJSON_Delete(s1); cJSON_Delete(e); oom = true; break;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObjectCS(s1, "sctp", sctp);
|
||||
cJSON_AddItemToObjectCS(e, "s1", s1);
|
||||
}
|
||||
|
||||
/* supported_ta_list (LTE TAC is 16-bit) */
|
||||
{
|
||||
cJSON *tas = cJSON_CreateArray();
|
||||
if (!tas) { cJSON_Delete(e); oom = true; break; }
|
||||
|
||||
bool inner_oom = false;
|
||||
|
||||
for (int t = 0; t < enb->num_of_supported_ta_list; t++) {
|
||||
cJSON *ta = cJSON_CreateObject();
|
||||
if (!ta) { inner_oom = true; break; }
|
||||
|
||||
char tac_hex[5];
|
||||
snprintf(tac_hex, sizeof tac_hex, "%04X", (unsigned)enb->supported_ta_list[t].tac);
|
||||
if (!cJSON_AddStringToObject(ta, "tac", tac_hex)) {
|
||||
cJSON_Delete(ta); inner_oom = true; break;
|
||||
}
|
||||
|
||||
char ta_plmn[OGS_PLMNIDSTRLEN] = {0};
|
||||
ogs_plmn_id_to_string(&enb->supported_ta_list[t].plmn_id, ta_plmn);
|
||||
if (!cJSON_AddStringToObject(ta, "plmn", ta_plmn)) {
|
||||
cJSON_Delete(ta); inner_oom = true; break;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(tas, ta);
|
||||
}
|
||||
|
||||
if (inner_oom) { cJSON_Delete(tas); cJSON_Delete(e); oom = true; break; }
|
||||
|
||||
cJSON_AddItemToObjectCS(e, "supported_ta_list", tas);
|
||||
}
|
||||
|
||||
/* num_connected_ues */
|
||||
if (!cJSON_AddNumberToObject(e, "num_connected_ues", (double)num_connected_ues)) {
|
||||
cJSON_Delete(e); oom = true; break;
|
||||
}
|
||||
|
||||
/* success -> append to items[] */
|
||||
cJSON_AddItemToArray(items, e);
|
||||
emitted++;
|
||||
idx++;
|
||||
}
|
||||
|
||||
json_pager_add_trailing(root, no_paging, page, page_size,
|
||||
emitted, has_next && !oom, "/enb-info", oom);
|
||||
|
||||
return json_pager_finalize(root, buf, buflen);
|
||||
}
|
||||
|
43
src/mme/enb-info.h
Normal file
43
src/mme/enb-info.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
* /enb-info — MME-side JSON exporter (Prometheus HTTP endpoint)
|
||||
*
|
||||
* License: AGPLv3+
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef ENB_INFO_PAGE_SIZE_DEFAULT
|
||||
#define ENB_INFO_PAGE_SIZE_DEFAULT 100U
|
||||
#endif
|
||||
|
||||
size_t mme_dump_enb_info(char *buf, size_t buflen);
|
||||
size_t mme_dump_enb_info_paged(char *buf, size_t buflen, size_t page, size_t page_size);
|
||||
void mme_metrics_enb_info_set_pager(size_t page, size_t page_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
@@ -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('''
|
||||
|
@@ -30,7 +30,10 @@
|
||||
#include "sgsap-path.h"
|
||||
#include "mme-gtp-path.h"
|
||||
#include "metrics.h"
|
||||
#include "connected_enbs.h"
|
||||
#include "metrics/prometheus/json_pager.h"
|
||||
#include "metrics/prometheus/pager.h"
|
||||
#include "enb-info.h"
|
||||
#include "ue-info.h"
|
||||
|
||||
static ogs_thread_t *thread;
|
||||
static void mme_main(void *data);
|
||||
@@ -67,7 +70,12 @@ int mme_initialize(void)
|
||||
if (rv != OGS_OK) return rv;
|
||||
|
||||
ogs_metrics_context_open(ogs_metrics_self());
|
||||
ogs_metrics_register_connected_enbs(mme_dump_connected_enbs);
|
||||
|
||||
/* dumpers /enb-info /ue-info */
|
||||
ogs_metrics_register_enb_info(mme_dump_enb_info);
|
||||
ogs_metrics_register_ue_info(mme_dump_ue_info);
|
||||
ogs_metrics_enb_info_set_pager = mme_metrics_enb_info_set_pager;
|
||||
ogs_metrics_ue_info_set_pager = mme_metrics_ue_info_set_pager;
|
||||
|
||||
rv = mme_fd_init();
|
||||
if (rv != OGS_OK) return OGS_ERROR;
|
||||
|
398
src/mme/ue-info.c
Normal file
398
src/mme/ue-info.c
Normal file
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
*
|
||||
* Connected MME UEs (LTE) JSON dumper for the Prometheus HTTP server (/ue-info).
|
||||
* - supi, cm_state, enb_info, location, ambr, slices, pdn_count
|
||||
* - pager: /ue-info?page=0&page_size=100 (0-based, page=-1 without paging) Default: page=0 page_size=100=MAXSIZE
|
||||
*
|
||||
* path: http://MME_IP:9090/ue-info
|
||||
*
|
||||
* curl -s "http://127.0.0.2:9090/ue-info?" |jq .
|
||||
* {
|
||||
* "items": [
|
||||
* {
|
||||
* "supi": "999700000021632",
|
||||
* "domain": "EPS",
|
||||
* "rat": "E-UTRA",
|
||||
* "cm_state": "connected",
|
||||
* "enb": {
|
||||
* "ostream_id": 3,
|
||||
* "mme_ue_ngap_id": 3,
|
||||
* "ran_ue_ngap_id": 9,
|
||||
* "enb_id": 264040,
|
||||
* "cell_id": 67594275
|
||||
* },
|
||||
* "location": {
|
||||
* "tai": {
|
||||
* "plmn": "99970",
|
||||
* "tac_hex": "0001",
|
||||
* "tac": 1
|
||||
* }
|
||||
* },
|
||||
* "ambr": {
|
||||
* "downlink": 1000000000,
|
||||
* "uplink": 1000000000
|
||||
* },
|
||||
* "pdn": [
|
||||
* {
|
||||
* "apn": "internet",
|
||||
* "qos_flows": [
|
||||
* {
|
||||
* "ebi": 5
|
||||
* }
|
||||
* ],
|
||||
* "qci": 9,
|
||||
* "ebi": 5,
|
||||
* "bearer_count": 1,
|
||||
* "pdu_state": "active"
|
||||
* }
|
||||
* ],
|
||||
* "pdn_count": 1
|
||||
* }
|
||||
* ],
|
||||
* "pager": {
|
||||
* "page": 0,
|
||||
* "page_size": 100,
|
||||
* "count": 1
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "ogs-core.h"
|
||||
#include "ogs-proto.h"
|
||||
|
||||
#include "mme-context.h"
|
||||
#include "ue-info.h"
|
||||
#include "mme-context.h"
|
||||
|
||||
#include "metrics/prometheus/pager.h"
|
||||
#include "metrics/prometheus/json_pager.h"
|
||||
#include "metrics/ogs-metrics.h"
|
||||
#include "sbi/openapi/external/cJSON.h"
|
||||
|
||||
#ifndef MME_UE_INFO_PAGE_SIZE_DEFAULT
|
||||
#define MME_UE_INFO_PAGE_SIZE_DEFAULT 100U
|
||||
#endif
|
||||
|
||||
static size_t g_ue_page = SIZE_MAX;
|
||||
static size_t g_ue_page_size = 0;
|
||||
|
||||
void mme_metrics_ue_info_set_pager(size_t page, size_t page_size)
|
||||
{
|
||||
g_ue_page = page;
|
||||
g_ue_page_size = page_size;
|
||||
}
|
||||
|
||||
size_t mme_dump_ue_info(char *buf, size_t buflen)
|
||||
{
|
||||
size_t page = g_ue_page;
|
||||
size_t page_size = g_ue_page_size;
|
||||
|
||||
if (page == SIZE_MAX) {
|
||||
page = 0;
|
||||
page_size = MME_UE_INFO_PAGE_SIZE_DEFAULT;
|
||||
} else if (page_size == 0) {
|
||||
page_size = MME_UE_INFO_PAGE_SIZE_DEFAULT;
|
||||
} else if (page_size > MME_UE_INFO_PAGE_SIZE_DEFAULT) {
|
||||
page_size = MME_UE_INFO_PAGE_SIZE_DEFAULT;
|
||||
}
|
||||
|
||||
return mme_dump_ue_info_paged(buf, buflen, page, page_size);
|
||||
}
|
||||
|
||||
static inline const char *cm_state_str(const mme_ue_t *ue)
|
||||
{
|
||||
return (ue && ECM_CONNECTED(ue)) ? "connected" : "idle";
|
||||
}
|
||||
|
||||
static cJSON *build_enb(const mme_ue_t *ue)
|
||||
{
|
||||
cJSON *enb = cJSON_CreateObject();
|
||||
if (!enb) return NULL;
|
||||
|
||||
/* ostream_id */
|
||||
if (!cJSON_AddNumberToObject(enb, "ostream_id", (double)ue->enb_ostream_id))
|
||||
goto end;
|
||||
|
||||
/* S1AP IDs */
|
||||
enb_ue_t *ran = enb_ue_find_by_id(ue->enb_ue_id);
|
||||
if (ran) {
|
||||
if (!cJSON_AddNumberToObject(enb, "mme_ue_ngap_id", (double)ran->mme_ue_s1ap_id))
|
||||
goto end;
|
||||
if (!cJSON_AddNumberToObject(enb, "ran_ue_ngap_id", (double)ran->enb_ue_s1ap_id))
|
||||
goto end;
|
||||
|
||||
mme_enb_t *enb_obj = mme_enb_find_by_id(ran->enb_id);
|
||||
if (enb_obj && enb_obj->enb_id_presence) {
|
||||
if (!cJSON_AddNumberToObject(enb, "enb_id", (double)enb_obj->enb_id))
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* cell_id: prefer last reported via E-UTRAN_CGI from RAN; else fallback to 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);
|
||||
}
|
||||
|
@@ -17,22 +17,25 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
* /connected-gnbs — MME-side JSON exporter (Prometheus HTTP endpoint)
|
||||
* /ue-info — MME-side JSON exporter (Prometheus HTTP endpoint)
|
||||
*
|
||||
* License: AGPLv3+
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* JSON dumper for /connected-gnbs.
|
||||
* Returns number of bytes written (<= buflen-1), buffer is always NUL-terminated.
|
||||
*/
|
||||
size_t mme_dump_connected_enbs(char *buf, size_t buflen);
|
||||
#ifndef UE_INFO_PAGE_SIZE_DEFAULT
|
||||
#define UE_INFO_PAGE_SIZE_DEFAULT 100U
|
||||
#endif
|
||||
|
||||
void mme_metrics_ue_info_set_pager(size_t page, size_t page_size);
|
||||
size_t mme_dump_ue_info(char *buf, size_t buflen);
|
||||
size_t mme_dump_ue_info_paged(char *buf, size_t buflen, size_t page, size_t page_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
@@ -1,336 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Connected UEs JSON dumper for the Prometheus HTTP server (/connected-ues).
|
||||
* - 5G PDUs: psi+dnn, snssai, qos_flows [{qfi,5qi}], pdu_state ("active"/"inactive"/"unknown")
|
||||
* - LTE PDUs: ebi(+psi if non-zero)+apn, qos_flows [{ebi,qci}], pdu_state ("unknown" at SMF scope)
|
||||
* - UE-level: ue_activity ("active" if any PDU active; "unknown" if none active but any unknown; else "idle")
|
||||
*/
|
||||
|
||||
/*
|
||||
* JSON dumper for /connected-gnbs (AMF)
|
||||
* Output (one item per connected gNB):
|
||||
* [
|
||||
* {
|
||||
* "supi": "imsi-999700000083810",
|
||||
* "pdu": [
|
||||
* {
|
||||
* "psi": 1,
|
||||
* "dnn": "internet",
|
||||
* "ipv4": "10.45.0.2",
|
||||
* "snssai": {
|
||||
* "sst": 1,
|
||||
* "sd": "ffffff"
|
||||
* },
|
||||
* "qos_flows": [
|
||||
* {
|
||||
* "qfi": 1,
|
||||
* "5qi": 9
|
||||
* }
|
||||
* ],
|
||||
* "pdu_state": "inactive"
|
||||
* }
|
||||
* ],
|
||||
* "ue_activity": "idle"
|
||||
* },
|
||||
* ]
|
||||
*/
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
* This file is part of Open5GS (AGPLv3+)
|
||||
*
|
||||
* JSON dumper for /connected-ues (SMF)
|
||||
* - 5G PDUs: psi+dnn, snssai, qos_flows [{qfi,5qi}], pdu_state
|
||||
* - LTE PDUs: ebi(+psi if non-zero)+apn, qos_flows [{ebi,qci}], pdu_state="unknown"
|
||||
* - UE-level: ue_activity derived from PDU states
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ogs-core.h" /* OGS_INET_NTOP, OGS_INET6_NTOP, OGS_ADDRSTRLEN, ogs_uint24_t */
|
||||
#include "context.h" /* smf_self(), smf_ue_t, smf_sess_t, smf_bearer_t, ogs_s_nssai_t */
|
||||
#include "connected_ues.h" /* size_t smf_dump_connected_ues(char *buf, size_t buflen) */
|
||||
|
||||
/* ------------------------- small helpers ------------------------- */
|
||||
|
||||
static inline size_t append_safe(char *buf, size_t off, size_t buflen, const char *fmt, ...)
|
||||
{
|
||||
if (!buf || off == (size_t)-1 || off >= buflen) return (size_t)-1;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
|
||||
int n = ogs_vsnprintf(buf + off, buflen - off, fmt, ap);
|
||||
#pragma GCC diagnostic pop
|
||||
va_end(ap);
|
||||
if (n < 0 || (size_t)n >= buflen - off) return (size_t)-1;
|
||||
return off + (size_t)n;
|
||||
}
|
||||
|
||||
/* Escapes \" \\ and control chars, emits: "key":"escaped" */
|
||||
static size_t append_json_kv_escaped(char *buf, size_t off, size_t buflen,
|
||||
const char *key, const char *val)
|
||||
{
|
||||
if (!val) val = "";
|
||||
off = append_safe(buf, off, buflen, "\"%s\":\"", key);
|
||||
if (off == (size_t)-1) return off;
|
||||
|
||||
for (const unsigned char *p = (const unsigned char *)val; *p; ++p) {
|
||||
unsigned char c = *p;
|
||||
if (c == '\\' || c == '\"') {
|
||||
off = append_safe(buf, off, buflen, "\\%c", c);
|
||||
} else if (c < 0x20) {
|
||||
off = append_safe(buf, off, buflen, "\\u%04x", (unsigned)c);
|
||||
} else {
|
||||
off = append_safe(buf, off, buflen, "%c", c);
|
||||
}
|
||||
if (off == (size_t)-1) return off;
|
||||
}
|
||||
return append_safe(buf, off, buflen, "\"");
|
||||
}
|
||||
|
||||
/* 24-bit helpers */
|
||||
static inline uint32_t u24_to_u32_portable(ogs_uint24_t v)
|
||||
{
|
||||
uint32_t x = 0;
|
||||
memcpy(&x, &v, sizeof(v) < sizeof(x) ? sizeof(v) : sizeof(x));
|
||||
return (x & 0xFFFFFFu);
|
||||
}
|
||||
|
||||
/* Emit a S-NSSAI object */
|
||||
static size_t append_snssai_obj(char *buf, size_t off, size_t buflen, const ogs_s_nssai_t *sn)
|
||||
{
|
||||
unsigned sst = (unsigned)sn->sst;
|
||||
uint32_t sd_u32 = u24_to_u32_portable(sn->sd);
|
||||
off = append_safe(buf, off, buflen, "{");
|
||||
off = append_safe(buf, off, buflen, "\"sst\":%u", sst);
|
||||
off = append_safe(buf, off, buflen, ",\"sd\":\"%06x\"}", (unsigned)(sd_u32 & 0xFFFFFFu));
|
||||
return off;
|
||||
}
|
||||
|
||||
/* ------------------------- state helpers ------------------------- */
|
||||
|
||||
static inline int up_state_of(const smf_sess_t *s) {
|
||||
if (!s) return 0;
|
||||
int u = (int)s->up_cnx_state;
|
||||
if (u == 0) u = (int)s->nsmf_param.up_cnx_state;
|
||||
return u;
|
||||
}
|
||||
|
||||
static inline bool has_n3_teid(const smf_sess_t *s) {
|
||||
return s && (s->remote_ul_teid != 0U || s->remote_dl_teid != 0U);
|
||||
}
|
||||
|
||||
static inline bool bearer_list_has_qfi(const smf_sess_t *s) {
|
||||
if (!s) return false;
|
||||
smf_bearer_t *b = NULL;
|
||||
ogs_list_for_each(&((smf_sess_t *)s)->bearer_list, b) {
|
||||
if (b && b->qfi > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Looks-5G heuristic: S-NSSAI present or any QFI bearer */
|
||||
static inline bool looks_5g_sess(const smf_sess_t *s) {
|
||||
if (!s) return false;
|
||||
if (s->s_nssai.sst != 0) return true;
|
||||
if (u24_to_u32_portable(s->s_nssai.sd) != 0) return true;
|
||||
if (bearer_list_has_qfi(s)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 5G PDU state */
|
||||
static const char *pdu_state_from_5g(const smf_sess_t *sess)
|
||||
{
|
||||
if (!sess) return "unknown";
|
||||
if ((int)sess->resource_status == (int)OpenAPI_resource_status_RELEASED)
|
||||
return "inactive";
|
||||
if (up_state_of(sess) == (int)OpenAPI_up_cnx_state_DEACTIVATED)
|
||||
return "inactive";
|
||||
if (sess->n1_released || sess->n2_released)
|
||||
return "inactive";
|
||||
if (!has_n3_teid(sess))
|
||||
return "inactive";
|
||||
return "active";
|
||||
}
|
||||
|
||||
/* LTE/EPC PDU state at SMF scope: unknown */
|
||||
static const char *pdu_state_from_4g(const smf_sess_t *sess)
|
||||
{
|
||||
(void)sess;
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/* QoS emitters */
|
||||
static size_t append_qos_info_5g(char *buf, size_t off, size_t buflen, const smf_sess_t *sess)
|
||||
{
|
||||
smf_bearer_t *b = NULL;
|
||||
bool first = true;
|
||||
off = append_safe(buf, off, buflen, ",\"qos_flows\":[");
|
||||
ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b) {
|
||||
if (!b || b->qfi == 0) continue;
|
||||
if (!first) off = append_safe(buf, off, buflen, ",");
|
||||
first = false;
|
||||
off = append_safe(buf, off, buflen, "{");
|
||||
off = append_safe(buf, off, buflen, "\"qfi\":%u", (unsigned)b->qfi);
|
||||
if (b->qos.index > 0)
|
||||
off = append_safe(buf, off, buflen, ",\"5qi\":%u", (unsigned)b->qos.index);
|
||||
off = append_safe(buf, off, buflen, "}");
|
||||
if (off == (size_t)-1) break;
|
||||
}
|
||||
off = append_safe(buf, off, buflen, "]");
|
||||
return off;
|
||||
}
|
||||
|
||||
static size_t append_qos_info_4g(char *buf, size_t off, size_t buflen, const smf_sess_t *sess)
|
||||
{
|
||||
smf_bearer_t *b = NULL;
|
||||
bool first = true;
|
||||
off = append_safe(buf, off, buflen, ",\"qos_flows\":[");
|
||||
ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b) {
|
||||
if (!b || b->ebi == 0) continue;
|
||||
if (!first) off = append_safe(buf, off, buflen, ",");
|
||||
first = false;
|
||||
|
||||
unsigned qci_val = (unsigned)b->qos.index;
|
||||
if (qci_val == 0 && sess) qci_val = (unsigned)sess->session.qos.index;
|
||||
|
||||
off = append_safe(buf, off, buflen, "{");
|
||||
off = append_safe(buf, off, buflen, "\"ebi\":%u", (unsigned)b->ebi);
|
||||
if (qci_val > 0)
|
||||
off = append_safe(buf, off, buflen, ",\"qci\":%u", qci_val);
|
||||
off = append_safe(buf, off, buflen, "}");
|
||||
if (off == (size_t)-1) break;
|
||||
}
|
||||
off = append_safe(buf, off, buflen, "]");
|
||||
return off;
|
||||
}
|
||||
|
||||
/* Macros for safe appends */
|
||||
#define APPF(...) do { off = append_safe(buf, off, buflen, __VA_ARGS__); if (off==(size_t)-1) goto trunc; } while(0)
|
||||
#define APPX(expr) do { off = (expr); if (off==(size_t)-1) goto trunc; } while(0)
|
||||
|
||||
/* ------------------------------- main ------------------------------- */
|
||||
|
||||
size_t smf_dump_connected_ues(char *buf, size_t buflen)
|
||||
{
|
||||
size_t off = 0;
|
||||
if (!buf || buflen == 0) return 0;
|
||||
|
||||
APPF("[");
|
||||
bool first_ue = true;
|
||||
|
||||
smf_ue_t *ue = NULL;
|
||||
ogs_list_for_each(&smf_self()->smf_ue_list, ue) {
|
||||
if (!ue) continue;
|
||||
|
||||
if (!first_ue) APPF(",");
|
||||
first_ue = false;
|
||||
|
||||
bool any_active = false, any_unknown = false;
|
||||
|
||||
APPF("{");
|
||||
|
||||
/* UE identity */
|
||||
const char *id = (ue->supi && ue->supi[0]) ? ue->supi :
|
||||
(ue->imsi_bcd[0] ? ue->imsi_bcd : "");
|
||||
APPX(append_json_kv_escaped(buf, off, buflen, "supi", id));
|
||||
|
||||
/* PDU array */
|
||||
APPF(",\"pdu\":[");
|
||||
bool first_pdu = true;
|
||||
|
||||
smf_sess_t *sess = NULL;
|
||||
ogs_list_for_each(&ue->sess_list, sess) {
|
||||
if (!sess) continue;
|
||||
const bool is_5g = looks_5g_sess(sess);
|
||||
const char *pstate = is_5g ? pdu_state_from_5g(sess)
|
||||
: pdu_state_from_4g(sess);
|
||||
|
||||
if (!first_pdu) APPF(",");
|
||||
first_pdu = false;
|
||||
|
||||
APPF("{");
|
||||
|
||||
if (is_5g) {
|
||||
/* 5G: PSI + DNN */
|
||||
APPF("\"psi\":%u,", (unsigned)sess->psi);
|
||||
APPX(append_json_kv_escaped(buf, off, buflen, "dnn",
|
||||
(sess->session.name ? sess->session.name : "")));
|
||||
} else {
|
||||
/* LTE: PSI if non-zero, EBI root + APN */
|
||||
unsigned ebi_root = 0;
|
||||
smf_bearer_t *b0 = NULL;
|
||||
ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b0) {
|
||||
if (b0 && b0->ebi > 0) { ebi_root = (unsigned)b0->ebi; break; }
|
||||
}
|
||||
if (sess->psi > 0) APPF("\"psi\":%u,", (unsigned)sess->psi);
|
||||
APPF("\"ebi\":%u,", ebi_root);
|
||||
APPX(append_json_kv_escaped(buf, off, buflen, "apn",
|
||||
(sess->session.name ? sess->session.name : "")));
|
||||
}
|
||||
|
||||
/* IPs if present */
|
||||
char ip4[OGS_ADDRSTRLEN] = "";
|
||||
char ip6[OGS_ADDRSTRLEN] = "";
|
||||
if (sess->ipv4) OGS_INET_NTOP(&sess->ipv4->addr, ip4);
|
||||
if (sess->ipv6) OGS_INET6_NTOP(&sess->ipv6->addr, ip6);
|
||||
if (ip4[0]) { APPF(","); APPX(append_json_kv_escaped(buf, off, buflen, "ipv4", ip4)); }
|
||||
if (ip6[0]) { APPF(","); APPX(append_json_kv_escaped(buf, off, buflen, "ipv6", ip6)); }
|
||||
|
||||
if (is_5g) {
|
||||
/* S-NSSAI */
|
||||
APPF(",\"snssai\":");
|
||||
APPX(append_snssai_obj(buf, off, buflen, &sess->s_nssai));
|
||||
/* QoS flows */
|
||||
APPX(append_qos_info_5g(buf, off, buflen, sess));
|
||||
} else {
|
||||
/* LTE QoS */
|
||||
APPX(append_qos_info_4g(buf, off, buflen, sess));
|
||||
}
|
||||
|
||||
APPF(",\"pdu_state\":\"%s\"", pstate);
|
||||
APPF("}");
|
||||
|
||||
if (strcmp(pstate, "active") == 0) any_active = true;
|
||||
else if (strcmp(pstate, "unknown") == 0) any_unknown = true;
|
||||
}
|
||||
APPF("]");
|
||||
|
||||
const char *ue_act = any_active ? "active" : (any_unknown ? "unknown" : "idle");
|
||||
APPF(",\"ue_activity\":\"%s\"", ue_act);
|
||||
|
||||
APPF("}");
|
||||
}
|
||||
|
||||
APPF("]");
|
||||
return off;
|
||||
|
||||
trunc:
|
||||
/* Minimal valid JSON on overflow */
|
||||
if (buf && buflen >= 3) { buf[0]='['; buf[1]=']'; buf[2]='\0'; return 2; }
|
||||
if (buf && buflen) buf[0]='\0';
|
||||
return 0;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -70,7 +70,7 @@ libsmf_sources = files('''
|
||||
ngap-path.h
|
||||
local-path.h
|
||||
metrics.h
|
||||
connected_ues.h
|
||||
pdu-info.h
|
||||
|
||||
init.c
|
||||
event.c
|
||||
@@ -113,7 +113,8 @@ libsmf_sources = files('''
|
||||
ngap-path.c
|
||||
local-path.c
|
||||
metrics.c
|
||||
connected_ues.c
|
||||
pdu-info.c
|
||||
../../lib/metrics/prometheus/json_pager.c
|
||||
'''.split())
|
||||
|
||||
libsmf = static_library('smf',
|
||||
|
410
src/smf/pdu-info.c
Normal file
410
src/smf/pdu-info.c
Normal file
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* Copyright (C) 2025 by Juraj Elias <juraj.elias@gmail.com>
|
||||
*
|
||||
* This file is part of Open5GS.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Connected PDUs JSON dumper for the Prometheus HTTP server (/pdu-info).
|
||||
* - 5G PDUs: psi+dnn, snssai, qos_flows [{qfi,5qi}], pdu_state ("active"/"inactive"/"unknown")
|
||||
* - LTE PDUs: ebi(+psi if non-zero)+apn, qos_flows [{ebi,qci}], pdu_state ("unknown" at SMF scope)
|
||||
* - UE-level: ue_activity ("active" if any PDU active; "unknown" if none active but any unknown; else "idle")
|
||||
* - pager: /pdu-info?page=0&page_size=100 (0-based, page=-1 without paging) Default: page=0 page_size=100=MAXSIZE
|
||||
*
|
||||
* path: http://SMF_IP:9090/pdu-info
|
||||
*
|
||||
* curl -s "http://127.0.0.4:9090/pdu-info?page_size=1" |jq .
|
||||
* {
|
||||
* "items": [
|
||||
* {
|
||||
* "supi": "imsi-231510000114763",
|
||||
* "pdu": [
|
||||
* {
|
||||
* "psi": 1,
|
||||
* "dnn": "internet",
|
||||
* "ipv4": "10.45.0.11",
|
||||
* "snssai": {
|
||||
* "sst": 1,
|
||||
* "sd": "ffffff"
|
||||
* },
|
||||
* "qos_flows": [
|
||||
* {
|
||||
* "qfi": 1,
|
||||
* "5qi": 9
|
||||
* }
|
||||
* ],
|
||||
* "pdu_state": "inactive"
|
||||
* }
|
||||
* ],
|
||||
* "ue_activity": "idle"
|
||||
* }
|
||||
* ],
|
||||
* "pager": {
|
||||
* "page": 0,
|
||||
* "page_size": 100,
|
||||
* "count": 1,
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include "ogs-core.h"
|
||||
#include "context.h"
|
||||
#include "pdu-info.h"
|
||||
#include "metrics/prometheus/pager.h"
|
||||
#include "sbi/openapi/external/cJSON.h"
|
||||
#include "metrics/prometheus/json_pager.h"
|
||||
|
||||
static size_t g_page = SIZE_MAX;
|
||||
static size_t g_page_size = 0;
|
||||
|
||||
static void smf_metrics_pdu_info_set_pager(size_t page, size_t page_size)
|
||||
{
|
||||
g_page = page;
|
||||
g_page_size = page_size;
|
||||
}
|
||||
|
||||
void smf_register_metrics_pager(void)
|
||||
{
|
||||
ogs_metrics_pdu_info_set_pager = smf_metrics_pdu_info_set_pager;
|
||||
}
|
||||
|
||||
static inline uint32_t u24_to_u32(ogs_uint24_t v)
|
||||
{
|
||||
uint32_t x = 0;
|
||||
memcpy(&x, &v, sizeof(v) < sizeof(x) ? sizeof(v) : sizeof(x));
|
||||
return (x & 0xFFFFFFu);
|
||||
}
|
||||
|
||||
static inline int up_state_of(const smf_sess_t *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
int u = (int)s->up_cnx_state;
|
||||
if (u == 0) u = (int)s->nsmf_param.up_cnx_state;
|
||||
return u;
|
||||
}
|
||||
|
||||
static inline bool has_n3_teid(const smf_sess_t *s)
|
||||
{
|
||||
return s && (s->remote_ul_teid != 0U || s->remote_dl_teid != 0U);
|
||||
}
|
||||
|
||||
static inline bool bearer_list_has_qfi(const smf_sess_t *s)
|
||||
{
|
||||
if (!s) return false;
|
||||
smf_bearer_t *b = NULL;
|
||||
ogs_list_for_each(&((smf_sess_t *)s)->bearer_list, b) {
|
||||
if (b && b->qfi > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 5G heuristic: S-NSSAI present or any QFI bearer */
|
||||
static inline bool looks_5g_sess(const smf_sess_t *s)
|
||||
{
|
||||
if (!s) return false;
|
||||
if (s->s_nssai.sst != 0) return true;
|
||||
if (u24_to_u32(s->s_nssai.sd) != 0) return true;
|
||||
if (bearer_list_has_qfi(s)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char *pdu_state_from_5g(const smf_sess_t *sess)
|
||||
{
|
||||
if (!sess) return "unknown";
|
||||
if ((int)sess->resource_status == (int)OpenAPI_resource_status_RELEASED)
|
||||
return "inactive";
|
||||
if (up_state_of(sess) == (int)OpenAPI_up_cnx_state_DEACTIVATED)
|
||||
return "inactive";
|
||||
if (sess->n1_released || sess->n2_released)
|
||||
return "inactive";
|
||||
if (!has_n3_teid(sess))
|
||||
return "inactive";
|
||||
return "active";
|
||||
}
|
||||
|
||||
/* LTE/EPC state at SMF scope: unknown */
|
||||
static const char *pdu_state_from_lte(const smf_sess_t *sess)
|
||||
{
|
||||
(void)sess;
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
static cJSON *build_snssai_object(const smf_sess_t *sess)
|
||||
{
|
||||
cJSON *sn = cJSON_CreateObject();
|
||||
if (!sn) return NULL;
|
||||
|
||||
cJSON *sst = cJSON_CreateNumber((double)sess->s_nssai.sst);
|
||||
if (!sst) { cJSON_Delete(sn); return NULL; }
|
||||
cJSON_AddItemToObjectCS(sn, "sst", sst);
|
||||
|
||||
char sd[7];
|
||||
snprintf(sd, sizeof sd, "%06x", (unsigned)u24_to_u32(sess->s_nssai.sd));
|
||||
cJSON *sdj = cJSON_CreateString(sd);
|
||||
if (!sdj) { cJSON_Delete(sn); return NULL; }
|
||||
cJSON_AddItemToObjectCS(sn, "sd", sdj);
|
||||
|
||||
return sn;
|
||||
}
|
||||
|
||||
static cJSON *build_qos_flows_array_5g(const smf_sess_t *sess)
|
||||
{
|
||||
cJSON *arr = cJSON_CreateArray();
|
||||
if (!arr) return NULL;
|
||||
|
||||
smf_bearer_t *b = NULL;
|
||||
ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b) {
|
||||
if (!b || b->qfi == 0) continue;
|
||||
|
||||
cJSON *q = cJSON_CreateObject();
|
||||
if (!q) { cJSON_Delete(arr); return NULL; }
|
||||
|
||||
cJSON *qfi = cJSON_CreateNumber((double)(unsigned)b->qfi);
|
||||
if (!qfi) { cJSON_Delete(q); cJSON_Delete(arr); return NULL; }
|
||||
cJSON_AddItemToObjectCS(q, "qfi", qfi);
|
||||
|
||||
if (b->qos.index > 0) {
|
||||
cJSON *q5 = cJSON_CreateNumber((double)(unsigned)b->qos.index);
|
||||
if (!q5) { cJSON_Delete(q); cJSON_Delete(arr); return NULL; }
|
||||
cJSON_AddItemToObjectCS(q, "5qi", q5);
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(arr, q);
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
static cJSON *build_qos_flows_array_lte(const smf_sess_t *sess)
|
||||
{
|
||||
cJSON *arr = cJSON_CreateArray();
|
||||
if (!arr) return NULL;
|
||||
|
||||
smf_bearer_t *b = NULL;
|
||||
ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b) {
|
||||
if (!b || b->ebi == 0) continue;
|
||||
|
||||
unsigned qci_val = (unsigned)b->qos.index;
|
||||
if (qci_val == 0) qci_val = (unsigned)sess->session.qos.index;
|
||||
|
||||
cJSON *q = cJSON_CreateObject();
|
||||
if (!q) { cJSON_Delete(arr); return NULL; }
|
||||
|
||||
cJSON *ebi = cJSON_CreateNumber((double)(unsigned)b->ebi);
|
||||
if (!ebi) { cJSON_Delete(q); cJSON_Delete(arr); return NULL; }
|
||||
cJSON_AddItemToObjectCS(q, "ebi", ebi);
|
||||
|
||||
if (qci_val > 0) {
|
||||
cJSON *qci = cJSON_CreateNumber((double)qci_val);
|
||||
if (!qci) { cJSON_Delete(q); cJSON_Delete(arr); return NULL; }
|
||||
cJSON_AddItemToObjectCS(q, "qci", qci);
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(arr, q);
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
static cJSON *build_single_pdu_object(const smf_sess_t *sess, int *any_active, int *any_unknown)
|
||||
{
|
||||
cJSON *pdu = cJSON_CreateObject();
|
||||
if (!pdu) return NULL;
|
||||
|
||||
/* 5G vs LTE fields */
|
||||
const bool is5g = looks_5g_sess(sess);
|
||||
if (is5g) {
|
||||
cJSON *psi = cJSON_CreateNumber((double)(unsigned)sess->psi);
|
||||
if (!psi) { cJSON_Delete(pdu); return NULL; }
|
||||
cJSON_AddItemToObjectCS(pdu, "psi", psi);
|
||||
|
||||
const char *dnn_c = (sess->session.name ? sess->session.name : "");
|
||||
cJSON *dnn = cJSON_CreateString(dnn_c);
|
||||
if (!dnn) { cJSON_Delete(pdu); return NULL; }
|
||||
cJSON_AddItemToObjectCS(pdu, "dnn", dnn);
|
||||
} else {
|
||||
if (sess->psi > 0) {
|
||||
cJSON *psi = cJSON_CreateNumber((double)(unsigned)sess->psi);
|
||||
if (!psi) { cJSON_Delete(pdu); return NULL; }
|
||||
cJSON_AddItemToObjectCS(pdu, "psi", psi);
|
||||
}
|
||||
|
||||
/* EBI root if present */
|
||||
unsigned ebi_root = 0;
|
||||
smf_bearer_t *b0 = NULL;
|
||||
ogs_list_for_each(&((smf_sess_t *)sess)->bearer_list, b0) {
|
||||
if (b0 && b0->ebi > 0) { ebi_root = (unsigned)b0->ebi; break; }
|
||||
}
|
||||
cJSON *ebi = cJSON_CreateNumber((double)ebi_root);
|
||||
if (!ebi) { cJSON_Delete(pdu); return NULL; }
|
||||
cJSON_AddItemToObjectCS(pdu, "ebi", ebi);
|
||||
|
||||
const char *apn_c = (sess->session.name ? sess->session.name : "");
|
||||
cJSON *apn = cJSON_CreateString(apn_c);
|
||||
if (!apn) { cJSON_Delete(pdu); return NULL; }
|
||||
cJSON_AddItemToObjectCS(pdu, "apn", apn);
|
||||
}
|
||||
|
||||
/* IPs */
|
||||
{
|
||||
char ip4[OGS_ADDRSTRLEN] = "";
|
||||
char ip6[OGS_ADDRSTRLEN] = "";
|
||||
if (sess->ipv4) OGS_INET_NTOP(&sess->ipv4->addr, ip4);
|
||||
if (sess->ipv6) OGS_INET6_NTOP(&sess->ipv6->addr, ip6);
|
||||
|
||||
if (ip4[0]) {
|
||||
cJSON *s = cJSON_CreateString(ip4);
|
||||
if (!s) { cJSON_Delete(pdu); return NULL; }
|
||||
cJSON_AddItemToObjectCS(pdu, "ipv4", s);
|
||||
}
|
||||
if (ip6[0]) {
|
||||
cJSON *s = cJSON_CreateString(ip6);
|
||||
if (!s) { cJSON_Delete(pdu); return NULL; }
|
||||
cJSON_AddItemToObjectCS(pdu, "ipv6", s);
|
||||
}
|
||||
}
|
||||
|
||||
/* S-NSSAI */
|
||||
{
|
||||
cJSON *sn = build_snssai_object(sess);
|
||||
if (!sn) { cJSON_Delete(pdu); return NULL; }
|
||||
cJSON_AddItemToObjectCS(pdu, "snssai", sn);
|
||||
}
|
||||
|
||||
/* QoS flows */
|
||||
{
|
||||
cJSON *qarr = is5g ? build_qos_flows_array_5g(sess)
|
||||
: build_qos_flows_array_lte(sess);
|
||||
if (!qarr) { cJSON_Delete(pdu); return NULL; }
|
||||
cJSON_AddItemToObjectCS(pdu, "qos_flows", qarr);
|
||||
}
|
||||
|
||||
/* PDU state + UE activity aggregation */
|
||||
{
|
||||
const char *state = is5g ? pdu_state_from_5g(sess) : pdu_state_from_lte(sess);
|
||||
cJSON *st = cJSON_CreateString(state);
|
||||
if (!st) { cJSON_Delete(pdu); return NULL; }
|
||||
cJSON_AddItemToObjectCS(pdu, "pdu_state", st);
|
||||
|
||||
if (any_active && !strcmp(state, "active")) *any_active = 1;
|
||||
else if (any_unknown && !strcmp(state, "unknown")) *any_unknown = 1;
|
||||
}
|
||||
|
||||
return pdu;
|
||||
}
|
||||
|
||||
static cJSON *build_ue_object(const smf_ue_t *ue)
|
||||
{
|
||||
cJSON *ueo = cJSON_CreateObject();
|
||||
if (!ueo) return NULL;
|
||||
|
||||
/* UE identity */
|
||||
const char *id = (ue->supi && ue->supi[0]) ? ue->supi :
|
||||
(ue->imsi_bcd[0] ? ue->imsi_bcd : "");
|
||||
cJSON *idj = cJSON_CreateString(id);
|
||||
if (!idj) { cJSON_Delete(ueo); return NULL; }
|
||||
cJSON_AddItemToObjectCS(ueo, "supi", idj);
|
||||
|
||||
/* PDUs */
|
||||
cJSON *pdus = cJSON_CreateArray();
|
||||
if (!pdus) { cJSON_Delete(ueo); return NULL; }
|
||||
|
||||
int any_active = 0, any_unknown = 0;
|
||||
|
||||
smf_sess_t *sess = NULL;
|
||||
ogs_list_for_each(&ue->sess_list, sess) {
|
||||
cJSON *pdu = build_single_pdu_object(sess, &any_active, &any_unknown);
|
||||
if (!pdu) { cJSON_Delete(pdus); cJSON_Delete(ueo); return NULL; }
|
||||
cJSON_AddItemToArray(pdus, pdu);
|
||||
}
|
||||
cJSON_AddItemToObjectCS(ueo, "pdu", pdus);
|
||||
|
||||
/* UE activity */
|
||||
{
|
||||
const char *ue_act = any_active ? "active" : (any_unknown ? "unknown" : "idle");
|
||||
cJSON *ua = cJSON_CreateString(ue_act);
|
||||
if (!ua) { cJSON_Delete(ueo); return NULL; }
|
||||
cJSON_AddItemToObjectCS(ueo, "ue_activity", ua);
|
||||
}
|
||||
|
||||
return ueo;
|
||||
}
|
||||
|
||||
size_t smf_dump_pdu_info_paged(char *buf, size_t buflen, size_t page, size_t page_size)
|
||||
{
|
||||
if (!buf || buflen == 0) return 0;
|
||||
|
||||
const bool no_paging = (page == SIZE_MAX);
|
||||
if (!no_paging) {
|
||||
if (page_size == 0) page_size = PDU_INFO_PAGE_SIZE_DEFAULT;
|
||||
if (page_size > PDU_INFO_PAGE_SIZE_DEFAULT) page_size = PDU_INFO_PAGE_SIZE_DEFAULT;
|
||||
} else {
|
||||
page_size = SIZE_MAX;
|
||||
page = 0;
|
||||
}
|
||||
|
||||
const size_t start_index = json_pager_safe_start_index(no_paging, page, page_size);
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (!root) { if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; } if (buflen) buf[0] = '\0'; return 0; }
|
||||
|
||||
cJSON *items = cJSON_CreateArray();
|
||||
if (!items) { cJSON_Delete(root); if (buflen >= 3) { memcpy(buf, "{}", 3); return 2; } if (buflen) buf[0] = '\0'; return 0; }
|
||||
|
||||
size_t idx = 0, emitted = 0;
|
||||
bool has_next = false, oom = false;
|
||||
|
||||
smf_context_t *smf = smf_self();
|
||||
smf_ue_t *ue = NULL;
|
||||
|
||||
ogs_list_for_each(&smf->smf_ue_list, ue) {
|
||||
int act = json_pager_advance(no_paging, idx, start_index, emitted, page_size, &has_next);
|
||||
if (act == 1) { idx++; continue; }
|
||||
if (act == 2) break;
|
||||
|
||||
cJSON *ueo = build_ue_object(ue);
|
||||
if (!ueo) { oom = true; break; }
|
||||
|
||||
cJSON_AddItemToArray(items, ueo);
|
||||
emitted++;
|
||||
idx++;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObjectCS(root, "items", items);
|
||||
json_pager_add_trailing(root, no_paging, page, page_size, emitted, has_next && !oom, "/pdu-info", oom);
|
||||
|
||||
return json_pager_finalize(root, buf, buflen);
|
||||
}
|
||||
|
||||
size_t smf_dump_pdu_info(char *buf, size_t buflen)
|
||||
{
|
||||
size_t page = g_page;
|
||||
size_t page_size = g_page_size;
|
||||
|
||||
if (page == SIZE_MAX) {
|
||||
page = 0;
|
||||
page_size = PDU_INFO_PAGE_SIZE_DEFAULT;
|
||||
} else if (page_size == 0) {
|
||||
page_size = PDU_INFO_PAGE_SIZE_DEFAULT;
|
||||
}
|
||||
|
||||
return smf_dump_pdu_info_paged(buf, buflen, page, page_size);
|
||||
}
|
||||
|
@@ -18,10 +18,10 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
* Minimal public API for connected_ues
|
||||
* Minimal public API for /pdu-info
|
||||
*/
|
||||
#ifndef SMF_CONNECTED_UES_H
|
||||
#define SMF_CONNECTED_UES_H
|
||||
#ifndef SMF_PDU_INFO_H
|
||||
#define SMF_PDU_INFO_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
@@ -29,14 +29,16 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Fills buf with a compact JSON array.
|
||||
* Returns the number of bytes written (excluding the terminating NUL). */
|
||||
size_t smf_dump_connected_ues(char *buf, size_t buflen);
|
||||
|
||||
#ifndef PDU_INFO_PAGE_SIZE_DEFAULT
|
||||
#define PDU_INFO_PAGE_SIZE_DEFAULT 100U
|
||||
#endif
|
||||
|
||||
void smf_register_metrics_pager(void);
|
||||
size_t smf_dump_pdu_info(char *buf, size_t buflen);
|
||||
size_t smf_dump_pdu_info_paged(char *buf, size_t buflen, size_t page, size_t page_size);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SMF_CONNECTED_UES_H */
|
||||
|
||||
|
||||
#endif
|
Reference in New Issue
Block a user