diff --git a/Makefile.conf.template b/Makefile.conf.template index b458133119..7edbde4ab4 100644 --- a/Makefile.conf.template +++ b/Makefile.conf.template @@ -38,6 +38,7 @@ #mi_xmlrpc_ng= New version of the xmlrpc server that handles xmlrpc requests and generates xmlrpc responses. | parsing/building XML library, typically libxml #mmgeoip= Lightweight wrapper for the MaxMind GeoIP API | libGeoIP #osp= Enables OpenSIPS to support secure, multi-lateral peering using the OSP standard | OSP development kit, typically osptoolkit +#opentelemetry= OpenTelemetry tracing for OpenSIPS routes | OpenTelemetry C++ SDK (opentelemetry-cpp) #perl= Easily implement your own OpenSIPS extensions in Perl | Perl library development files, typically libperl-dev #pi_http= Provides a simple web database provisioning interface | XML parsing & building library, typically libxml-dev #rabbitmq_consumer= Receive AMQP messages which will be delivered by triggering events | RabbitMQ development library, librabbitmq-dev @@ -75,7 +76,7 @@ #uuid= UUID generator | uuid-dev # the below definition must be one single line (no wrapping) to make the "menuconfig" tool happy -exclude_modules?= aaa_diameter aaa_radius auth_jwt auth_web3 b2b_logic_xml cachedb_cassandra cachedb_couchbase cachedb_dynamodb cachedb_memcached cachedb_mongodb cachedb_redis carrierroute cgrates compression cpl_c db_berkeley db_http db_mysql db_oracle db_perlvdb db_postgres db_sqlite db_unixodbc dialplan emergency event_rabbitmq event_kafka event_sqs h350 httpd http2d identity jabber json launch_darkly ldap lua mi_xmlrpc_ng mmgeoip osp perl pi_http presence presence_dialoginfo presence_mwi presence_reginfo presence_xml presence_dfks proto_ipsec proto_sctp proto_tls proto_wss pua pua_bla pua_dialoginfo pua_mi pua_reginfo pua_usrloc pua_xmpp python regex rabbitmq_consumer rest_client rls rtp.io siprec sngtc snmpstats stir_shaken tls_mgm tls_openssl tls_wolfssl uuid xcap xcap_client xml xmpp +exclude_modules?= aaa_diameter aaa_radius auth_jwt auth_web3 b2b_logic_xml cachedb_cassandra cachedb_couchbase cachedb_dynamodb cachedb_memcached cachedb_mongodb cachedb_redis carrierroute cgrates compression cpl_c db_berkeley db_http db_mysql db_oracle db_perlvdb db_postgres db_sqlite db_unixodbc dialplan emergency event_rabbitmq event_kafka event_sqs h350 httpd http2d identity jabber json launch_darkly ldap lua mi_xmlrpc_ng mmgeoip opentelemetry osp perl pi_http presence presence_dialoginfo presence_mwi presence_reginfo presence_xml presence_dfks proto_ipsec proto_sctp proto_tls proto_wss pua pua_bla pua_dialoginfo pua_mi pua_reginfo pua_usrloc pua_xmpp python regex rabbitmq_consumer rest_client rls rtp.io siprec sngtc snmpstats stir_shaken tls_mgm tls_openssl tls_wolfssl uuid xcap xcap_client xml xmpp include_modules?= diff --git a/action.c b/action.c index a803d3b35b..9a90861215 100644 --- a/action.c +++ b/action.c @@ -60,6 +60,7 @@ #include "script_var.h" #include "xlog.h" #include "cfg_pp.h" +#include "route_trace.h" #include @@ -215,6 +216,8 @@ int run_top_route(struct script_route sr, struct sip_msg* msg) int bk_action_flags, route_stack_start_bkp = -1, route_stack_size_bkp; int ret; context_p ctx = NULL; + const char *trace_file = NULL; + int trace_line = 0; bk_action_flags = action_flags; @@ -251,9 +254,27 @@ int run_top_route(struct script_route sr, struct sip_msg* msg) else route_stack[route_stack_start] = sr.name; + if (route_trace_enabled()) { + if (sr.a && sr.a->file) { + trace_file = sr.a->file; + trace_line = sr.a->line; + } + route_trace_msg_start(msg, route_type, route_stack[route_stack_start], + route_stack_size, route_stack_start); + route_trace_route_enter(msg, route_type, route_stack[route_stack_start], + trace_file, trace_line, route_stack_size, route_stack_start); + } + run_actions(sr.a, msg); ret = action_flags; + if (route_trace_enabled()) { + route_trace_route_exit(msg, route_type, route_stack[route_stack_start], + NULL, 0, route_stack_size, route_stack_start, ret); + route_trace_msg_end(msg, route_type, route_stack[route_stack_start], + route_stack_size, route_stack_start, ret); + } + if (route_stack_start_bkp != -1) { route_stack_size = route_stack_size_bkp; route_stack_start = route_stack_start_bkp; @@ -808,12 +829,28 @@ int do_action(struct action* a, struct sip_msg* msg) } route_params_push_level(sroutes->request[i].name, route_p, (void *)(unsigned long)len, route_param_get); + if (route_trace_enabled()) + route_trace_route_enter(msg, route_type, + sroutes->request[i].name, a->file, a->line, + route_stack_size, route_stack_start); return_code=run_actions(sroutes->request[i].a, msg); + if (route_trace_enabled()) + route_trace_route_exit(msg, route_type, + sroutes->request[i].name, a->file, a->line, + route_stack_size, route_stack_start, return_code); route_params_release(route_p, len); route_params_pop_level(); } else { route_params_push_level(sroutes->request[i].name, NULL, 0, route_param_get); + if (route_trace_enabled()) + route_trace_route_enter(msg, route_type, + sroutes->request[i].name, a->file, a->line, + route_stack_size, route_stack_start); return_code=run_actions(sroutes->request[i].a, msg); + if (route_trace_enabled()) + route_trace_route_exit(msg, route_type, + sroutes->request[i].name, a->file, a->line, + route_stack_size, route_stack_start, return_code); route_params_pop_level(); } ret=return_code; diff --git a/dprint.h b/dprint.h index ea5ab2cd79..2fed36585e 100644 --- a/dprint.h +++ b/dprint.h @@ -105,11 +105,12 @@ #undef NO_LOG #endif -#define MAX_LOG_CONS_NO 3 +#define MAX_LOG_CONS_NO 4 #define STDERR_CONSUMER_NAME "stderror" #define SYSLOG_CONSUMER_NAME "syslog" #define EVENT_CONSUMER_NAME "event" +#define OTEL_CONSUMER_NAME "opentelemetry" #define LOG_PLAIN_NAME "plain_text" #define LOG_JSON_NAME "json" diff --git a/modules/opentelemetry/Makefile b/modules/opentelemetry/Makefile new file mode 100644 index 0000000000..76ab1e1567 --- /dev/null +++ b/modules/opentelemetry/Makefile @@ -0,0 +1,27 @@ +# WARNING: do not run this directly, it should be run by the master Makefile + +include ../../Makefile.defs + +NAME=opentelemetry.so + +CXX?=g++ + +# Make sure the C++ object participates in linking and overrides any defaults +# from the shared Makefile logic (which only considers .c sources). +override extra_objs+=opentelemetry.o + +# Always use the system-installed OpenTelemetry C++ SDK (via pkg-config). +OTEL_CPP_CFLAGS:=$(shell pkg-config --cflags opentelemetry_trace opentelemetry_resources opentelemetry_common opentelemetry_api) +OTEL_CPP_LIBS:=$(shell pkg-config --libs opentelemetry_trace opentelemetry_resources opentelemetry_logs opentelemetry_metrics opentelemetry_common) -lopentelemetry_exporter_otlp_http +ifeq ($(strip $(OTEL_CPP_CFLAGS)$(OTEL_CPP_LIBS)),) +$(error OpenTelemetry C++ SDK not found via pkg-config (opentelemetry_*). Please install the system libraries.) +endif + +DEFS+=-DHAVE_OPENTELEMETRY_CPP +CXXFLAGS+=-std=c++17 -fpermissive $(OTEL_CPP_CFLAGS) +LIBS+=$(OTEL_CPP_LIBS) + +include ../../Makefile.modules + +opentelemetry.o: opentelemetry.cpp + $(Q)$(CXX) $(CXXFLAGS) $(DEFS) $(INCLUDE) -fPIC -c $< -o $@ diff --git a/modules/opentelemetry/doc/opentelemetry.xml b/modules/opentelemetry/doc/opentelemetry.xml new file mode 100644 index 0000000000..a5e579e9df --- /dev/null +++ b/modules/opentelemetry/doc/opentelemetry.xml @@ -0,0 +1,27 @@ + + + + + + +%docentities; + +]> + + + + opentelemetry Module + &osipsname; + + + + &admin; + &contrib; + + &docCopyrights; + ©right; 2026 &osipsproj; + + diff --git a/modules/opentelemetry/doc/opentelemetry_admin.xml b/modules/opentelemetry/doc/opentelemetry_admin.xml new file mode 100644 index 0000000000..984db27ae2 --- /dev/null +++ b/modules/opentelemetry/doc/opentelemetry_admin.xml @@ -0,0 +1,203 @@ + + + + + &adminguide; + +
+ Overview + + The opentelemetry module provides OpenTelemetry + tracing for &osips; route execution. It creates a root span per + processed SIP message and a child span for each route entry. + + + Spans include common SIP attributes (method, Call-ID, CSeq, status) and + message metadata. While a span is active, &osips; logs can be attached + as OpenTelemetry events for easier correlation. + + + Trace data is exported via the OTLP/HTTP exporter from the + OpenTelemetry C++ SDK. + +
+ +
+ Dependencies +
+ &osips; Modules + + The following modules must be loaded before this module: + + + + None. + + + + +
+ +
+ External Libraries or Applications + + The following libraries or applications must be installed before + running &osips; with this module loaded: + + + + OpenTelemetry C++ SDK (opentelemetry-cpp), + with the OTLP/HTTP exporter enabled. + + + + +
+
+ +
+ Exported Parameters + +
+ <varname>enable</varname> (integer) + + Enables or disables OpenTelemetry tracing at startup. It can also be + changed at runtime using the otel_enable + MI command. + + + If &osips; was built without the OpenTelemetry C++ SDK, enabling this + parameter will fail at startup. + + + + Default value is 0 (disabled). + + + + Set <varname>enable</varname> parameter + +... +modparam("opentelemetry", "enable", 1) +... + + +
+ +
+ <varname>log_level</varname> (integer) + + Log level threshold used by the OpenTelemetry log consumer when + attaching log events to the active span. + + + + Default value is L_DBG. + + + + Set <varname>log_level</varname> parameter + +... +modparam("opentelemetry", "log_level", 3) +... + + +
+ +
+ <varname>use_batch</varname> (integer) + + Selects the OpenTelemetry span processor. When enabled, the module uses + the batch span processor; otherwise it uses the simple span processor. + + + + Default value is 1 (enabled). + + + + Set <varname>use_batch</varname> parameter + +... +modparam("opentelemetry", "use_batch", 0) +... + + +
+ +
+ <varname>service_name</varname> (string) + + Sets the OpenTelemetry service.name resource attribute. + + + + Default value is opensips. + + + + Set <varname>service_name</varname> parameter + +... +modparam("opentelemetry", "service_name", "edge-proxy") +... + + +
+ +
+ <varname>exporter_endpoint</varname> (string) + + Overrides the OTLP/HTTP exporter endpoint. If empty, the OpenTelemetry + SDK default is used. + + + + Default value is empty. + + + + Set <varname>exporter_endpoint</varname> parameter + +... +modparam("opentelemetry", "exporter_endpoint", "http://127.0.0.1:4318/v1/traces") +... + + +
+
+ +
+ Exported MI Functions + +
+ + <function moreinfo="none">otel_enable</function> + + + Enables or disables OpenTelemetry tracing at runtime. + + + Name: otel_enable + + Parameters: + + + enable - set to 1 to enable + tracing or 0 to disable it. + + + + MI FIFO Command Format: + + + ## enable tracing + opensips-cli -x mi otel_enable enable=1 + ## disable tracing + opensips-cli -x mi otel_enable enable=0 + +
+
+ +
diff --git a/modules/opentelemetry/opentelemetry.cpp b/modules/opentelemetry/opentelemetry.cpp new file mode 100644 index 0000000000..1231ab6eb5 --- /dev/null +++ b/modules/opentelemetry/opentelemetry.cpp @@ -0,0 +1,714 @@ +/* + * OpenTelemetry tracing for OpenSIPS routes + * + * Copyright (C) 2026 OpenSIPS Project + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * opensips 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * History: + * -------- + * 2026-01-05 initial release (vlad) + */ + + +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_OPENTELEMETRY_CPP +#include "opentelemetry/trace/provider.h" +#include "opentelemetry/trace/scope.h" +#include "opentelemetry/trace/span.h" +#include "opentelemetry/trace/span_context.h" +#include "opentelemetry/trace/tracer.h" +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/trace/batch_span_processor.h" +#include "opentelemetry/sdk/trace/simple_processor.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h" +#include "opentelemetry/exporters/otlp/otlp_http_exporter_options.h" +namespace oteltrace = opentelemetry::trace; +namespace otelsdktrace = opentelemetry::sdk::trace; +namespace otelsdkresource = opentelemetry::sdk::resource; +namespace otelotlp = opentelemetry::exporter::otlp; +#endif + +#ifdef __cplusplus +/* Relax C-only headers for C++ compilation. */ +#define class class_keyword +#undef HAVE_STDATOMIC +#undef HAVE_GENERICS +#endif + +extern "C" { +#include "../../poll_types.h" +#include "../../sr_module.h" +#include "../../dprint.h" +#include "../../mem/mem.h" +#include "../../mem/shm_mem.h" +#include "../../route_trace.h" +#include "../../log_interface.h" +#include "../../str.h" +#include "../../pt.h" +#include "../../version.h" +#include "../../ip_addr.h" +#include "../../mi/mi.h" +} + +#ifdef class +#undef class +#endif + +static int otel_enabled_cfg = 0; +static int *otel_enabled = NULL; +static int otel_log_level = L_DBG; +static int otel_use_batch = 1; +static str otel_service_name = str_init("opensips"); +static str otel_exporter_endpoint = STR_NULL; + +struct otel_span { +#ifdef HAVE_OPENTELEMETRY_CPP + opentelemetry::nostd::shared_ptr span; + std::unique_ptr scope; +#endif + const char *name; + int route_type; + int depth; + int is_root; + const char *file; + int line; + struct otel_span *parent; +}; + +static __thread struct otel_span *otel_span_top; +static __thread int otel_log_in_cb; + +static __thread route_trace_ctx_t otel_parent_ctx; +static __thread int otel_parent_ctx_set; +static int otel_trace_registered; +static int otel_log_consumer_registered; + +#ifdef HAVE_OPENTELEMETRY_CPP +static opentelemetry::nostd::shared_ptr otel_tracer; +static opentelemetry::nostd::shared_ptr otel_provider; +#endif + +static int mod_init(void); +static int child_init(int rank); +static void destroy(void); +static mi_response_t *otel_mi_enable(const mi_params_t *params, + struct mi_handler *async_hdl); +static void otel_log_consumer(int level, int facility, const char *module, + const char *func, char *format, va_list ap); +static int otel_ensure_provider(void); +extern route_trace_handlers_t otel_trace_handlers; +#ifdef HAVE_OPENTELEMETRY_CPP +static int otel_init_provider(void); +#endif +static void otel_span_reset(void); + +static inline int otel_is_enabled(void) +{ + return otel_enabled && *otel_enabled; +} + +static void otel_parent_ctx_clear(void) +{ + memset(&otel_parent_ctx, 0, sizeof(otel_parent_ctx)); + otel_parent_ctx_set = 0; +} + +static int otel_ensure_provider(void) +{ +#ifdef HAVE_OPENTELEMETRY_CPP + if (otel_is_enabled() && !otel_tracer) { + if (otel_init_provider() != 0) { + LM_ERR("failed to initialize tracer provider\n"); + return -1; + } + } +#endif + return 0; +} + +static int otel_runtime_set_enable(int enable) +{ + if (enable) { + if (otel_is_enabled()) + return 0; + if (otel_enabled) + *otel_enabled = 1; + otel_enabled_cfg = 1; + + if (otel_ensure_provider() != 0) { + if (otel_enabled) + *otel_enabled = 0; + otel_enabled_cfg = 0; + return -1; + } + return 0; + } + + if (!otel_is_enabled()) + return 0; + + if (otel_enabled) + *otel_enabled = 0; + otel_enabled_cfg = 0; + + otel_span_reset(); + otel_parent_ctx_clear(); + + return 0; +} + +static inline const char *route_type_name(int route_type) +{ + switch (route_type) { + case REQUEST_ROUTE: return "request"; + case FAILURE_ROUTE: return "failure"; + case ONREPLY_ROUTE: return "onreply"; + case BRANCH_ROUTE: return "branch"; + case ERROR_ROUTE: return "error"; + case LOCAL_ROUTE: return "local"; + case STARTUP_ROUTE: return "startup"; + case TIMER_ROUTE: return "timer"; + case EVENT_ROUTE: return "event"; + default: return "unknown"; + } +} + +static int otel_get_ctx(route_trace_ctx_t *ctx) +{ + if (!ctx) + return 0; + memset(ctx, 0, sizeof(*ctx)); +#ifdef HAVE_OPENTELEMETRY_CPP + if (otel_span_top && otel_span_top->span) { + auto sc = otel_span_top->span->GetContext(); + if (!sc.IsValid()) + return 0; + memcpy(ctx->trace_id, sc.trace_id().Id().data(), sizeof(ctx->trace_id)); + memcpy(ctx->span_id, sc.span_id().Id().data(), sizeof(ctx->span_id)); + ctx->trace_flags = sc.trace_flags().flags(); + return 1; + } +#endif + return 0; +} + +static int otel_set_ctx(const route_trace_ctx_t *ctx) +{ + if (!ctx) + return 0; + memcpy(&otel_parent_ctx, ctx, sizeof(otel_parent_ctx)); + otel_parent_ctx_set = 1; + return 1; +} + +static void otel_span_reset(void) +{ + struct otel_span *span, *next; + + span = otel_span_top; + while (span) { + next = span->parent; +#ifdef HAVE_OPENTELEMETRY_CPP + if (span->span) + span->span->End(); + span->scope.reset(); +#endif + span->~otel_span(); + pkg_free(span); + span = next; + } + + otel_span_top = NULL; +} + +#ifdef HAVE_OPENTELEMETRY_CPP +static int otel_init_provider(void) +{ + std::string service_name(otel_service_name.s ? otel_service_name.s : "opensips", + otel_service_name.s ? otel_service_name.len : (int)strlen("opensips")); + + otelsdkresource::ResourceAttributes attrs = { + { "service.name", service_name }, + { "process.pid", (int64_t)my_pid() } + }; + + auto resource = otelsdkresource::Resource::Create(attrs); + + otelotlp::OtlpHttpExporterOptions opts; + if (otel_exporter_endpoint.len && otel_exporter_endpoint.s) + opts.url = std::string(otel_exporter_endpoint.s, otel_exporter_endpoint.len); + + auto exporter = otelotlp::OtlpHttpExporterFactory::Create(opts); + std::unique_ptr processor; + + if (otel_use_batch) { + otelsdktrace::BatchSpanProcessorOptions bs_opts; + processor = std::make_unique(std::move(exporter), bs_opts); + } else { + processor = std::make_unique(std::move(exporter)); + } + + auto provider = opentelemetry::nostd::shared_ptr( + std::make_shared(std::move(processor), resource)); + oteltrace::Provider::SetTracerProvider(provider); + otel_tracer = provider->GetTracer("opensips.opentelemetry", OPENSIPS_FULL_VERSION); + otel_provider = provider; + + return 0; +} + +static void otel_set_msg_attributes(struct sip_msg *msg, oteltrace::Span *span) +{ + if (!span || !msg) + return; + + if (msg->first_line.type == SIP_REQUEST) { + str *m = &msg->first_line.u.request.method; + span->SetAttribute("sip.method", opentelemetry::nostd::string_view(m->s, m->len)); + span->SetAttribute("opensips.top_route", route_type_name(REQUEST_ROUTE)); + if (msg->first_line.u.request.uri.s && msg->first_line.u.request.uri.len) + span->SetAttribute("sip.ruri", + opentelemetry::nostd::string_view(msg->first_line.u.request.uri.s, + msg->first_line.u.request.uri.len)); + } else if (msg->first_line.type == SIP_REPLY) { + span->SetAttribute("sip.status_code", (int64_t)msg->first_line.u.reply.statuscode); + if (msg->first_line.u.reply.reason.s && msg->first_line.u.reply.reason.len) + span->SetAttribute("sip.reason", + opentelemetry::nostd::string_view(msg->first_line.u.reply.reason.s, + msg->first_line.u.reply.reason.len)); + span->SetAttribute("opensips.top_route", route_type_name(ONREPLY_ROUTE)); + } + + if (msg->callid && msg->callid->body.s && msg->callid->body.len) + span->SetAttribute("sip.call_id", + opentelemetry::nostd::string_view(msg->callid->body.s, msg->callid->body.len)); + + if (msg->cseq && msg->cseq->body.s && msg->cseq->body.len) + span->SetAttribute("sip.cseq", + opentelemetry::nostd::string_view(msg->cseq->body.s, msg->cseq->body.len)); + + if (msg->via1 && msg->via1->host.s && msg->via1->host.len) + span->SetAttribute("net.host.ip", + opentelemetry::nostd::string_view(msg->via1->host.s, msg->via1->host.len)); + + span->SetAttribute("net.peer.ip", ip_addr2a(&msg->rcv.src_ip)); + span->SetAttribute("net.peer.port", (int64_t)msg->rcv.src_port); +} +#endif + +static struct otel_span *otel_span_start(const char *name, int route_type, + int depth, int is_root, const char *file, int line) +{ + struct otel_span *span; + int has_parent_ctx = 0; + int has_parent_link = 0; + + if (!otel_is_enabled()) + return NULL; + + if (otel_ensure_provider() != 0) + return NULL; + + span = (struct otel_span *)pkg_malloc(sizeof *span); + if (!span) + return NULL; + new (span) otel_span(); + +#ifdef HAVE_OPENTELEMETRY_CPP + if (otel_tracer) { + oteltrace::StartSpanOptions opts; + opts.kind = oteltrace::SpanKind::kInternal; + if (otel_span_top && otel_span_top->span) { + opts.parent = otel_span_top->span->GetContext(); + has_parent_ctx = 1; + has_parent_link = 1; + } else if (otel_parent_ctx_set) { + has_parent_ctx = 1; + opentelemetry::trace::TraceId tid(opentelemetry::nostd::span(otel_parent_ctx.trace_id, 16)); + opentelemetry::trace::SpanId sid(opentelemetry::nostd::span(otel_parent_ctx.span_id, 8)); + opentelemetry::trace::TraceFlags tf(otel_parent_ctx.trace_flags); + opentelemetry::trace::SpanContext sc(tid, sid, tf, true); + if (sc.IsValid()) { + opts.parent = sc; + has_parent_link = 1; + if (otel_parent_ctx.has_start_time) { + opts.start_system_time = opentelemetry::common::SystemTimestamp( + std::chrono::nanoseconds(otel_parent_ctx.start_system_ns)); + opts.start_steady_time = opentelemetry::common::SteadyTimestamp( + std::chrono::nanoseconds(otel_parent_ctx.start_steady_ns)); + } + } + otel_parent_ctx_clear(); + } + + auto s = otel_tracer->StartSpan(name ? name : "", opts); + s->SetAttribute("opensips.route.type", route_type_name(route_type)); + s->SetAttribute("opensips.route.is_root", (int64_t)(has_parent_link ? 0 : is_root)); + if (file) { + s->SetAttribute("code.filepath", file); + s->SetAttribute("code.lineno", line); + } + + span->scope = std::unique_ptr(new oteltrace::Scope(s)); + span->span = s; + } +#endif + + span->name = name; + span->route_type = route_type; + span->depth = depth; + span->is_root = is_root; + span->file = file; + span->line = line; + span->parent = otel_span_top; + + otel_span_top = span; + + return span; +} + +static void otel_span_end(struct otel_span *span) +{ + if (!span) + return; + +#ifdef HAVE_OPENTELEMETRY_CPP + if (span->span) + span->span->End(); + span->scope.reset(); + span->span = nullptr; +#endif + + otel_span_top = span->parent; + span->~otel_span(); + pkg_free(span); +} + +static void otel_on_msg_start(struct sip_msg *msg, int route_type, + const char *route_name, int stack_size, int stack_start) +{ + const char *name; + + if (!otel_is_enabled()) + return; + + if (otel_ensure_provider() != 0) + return; + + if (otel_parent_ctx_set) + return; + + otel_span_reset(); + + name = ""; + otel_span_start(name, route_type, stack_size - stack_start, 1, NULL, 0); + +#ifdef HAVE_OPENTELEMETRY_CPP + if (otel_span_top && otel_span_top->span) { + otel_set_msg_attributes(msg, otel_span_top->span.get()); + otel_span_top->span->SetAttribute("sip.raw", + opentelemetry::nostd::string_view(msg->buf, msg->len)); + } +#endif + + (void)msg; +} + +static void otel_on_msg_end(struct sip_msg *msg, int route_type, + const char *route_name, int stack_size, int stack_start, int status) +{ + if (!otel_is_enabled()) + return; + + if (otel_ensure_provider() != 0) + return; + + (void)msg; + (void)route_type; + (void)route_name; + (void)stack_size; + (void)stack_start; + (void)status; + + otel_span_reset(); +} + +static void otel_on_route_enter(struct sip_msg *msg, int route_type, + const char *route_name, const char *file, int line, + int stack_size, int stack_start) +{ + const char *name; + + if (!otel_is_enabled()) + return; + + if (otel_ensure_provider() != 0) + return; + + name = route_name ? route_name : ""; + otel_span_start(name, route_type, stack_size - stack_start, 0, file, line); + + (void)msg; +} + +static void otel_on_route_exit(struct sip_msg *msg, int route_type, + const char *route_name, const char *file, int line, + int stack_size, int stack_start, int status) +{ + if (!otel_is_enabled()) + return; + + if (otel_ensure_provider() != 0) + return; + + (void)msg; + (void)route_type; + (void)route_name; + (void)file; + (void)line; + (void)stack_size; + (void)stack_start; + + otel_span_end(otel_span_top); +} + +static const char *level_to_str(int level) +{ + switch (level) { + case L_ALERT: return "alert"; + case L_CRIT: return "crit"; + case L_ERR: return "error"; + case L_WARN: return "warn"; + case L_NOTICE: return "notice"; + case L_INFO: return "info"; + case L_DBG: return "debug"; + default: return "unknown"; + } +} + +static void otel_log_consumer(int level, int facility, const char *module, + const char *func, char *format, va_list ap) +{ + char buf[512]; + int len; + va_list ap_copy; + + if (!otel_is_enabled() || otel_log_in_cb) + return; + + if (otel_ensure_provider() != 0) + return; + + if (!otel_span_top) + return; + + otel_log_in_cb = 1; + + va_copy(ap_copy, ap); + len = vsnprintf(buf, sizeof(buf), format, ap_copy); + va_end(ap_copy); + + if (len < 0) { + otel_log_in_cb = 0; + return; + } + + if (len >= (int)sizeof(buf)) + len = sizeof(buf) - 1; + + buf[len] = '\0'; + +#ifdef HAVE_OPENTELEMETRY_CPP + if (otel_span_top->span) { + otel_span_top->span->AddEvent("log", { + { "log.level", level_to_str(level) }, + { "log.message", buf } + }); + } +#else + (void)level; + (void)facility; + (void)module; + (void)func; + (void)format; +#endif + + (void)facility; + + otel_log_in_cb = 0; +} + +route_trace_handlers_t otel_trace_handlers = { + .on_msg_start = otel_on_msg_start, + .on_msg_end = otel_on_msg_end, + .on_route_enter = otel_on_route_enter, + .on_route_exit = otel_on_route_exit, + .get_ctx = otel_get_ctx, + .set_ctx = otel_set_ctx, +}; + +static const param_export_t params[] = { + { "enable", INT_PARAM, &otel_enabled_cfg }, + { "log_level", INT_PARAM, &otel_log_level }, + { "use_batch", INT_PARAM, &otel_use_batch }, + { "service_name", STR_PARAM, &otel_service_name.s }, + { "exporter_endpoint", STR_PARAM, &otel_exporter_endpoint.s }, + { 0, 0, 0 } +}; + +static const mi_export_t mi_cmds[] = { + { "otel_enable", 0, 0, 0, { + { otel_mi_enable, { "enable", 0 } }, + { EMPTY_MI_RECIPE } + } + }, + { EMPTY_MI_EXPORT } +}; + +extern "C" struct module_exports exports = { + "opentelemetry", + MOD_TYPE_DEFAULT, + { OPENSIPS_FULL_VERSION, OPENSIPS_COMPILE_FLAGS, { VERSIONTYPE, THISREVISION } }, + DEFAULT_DLFLAGS, + 0, + 0, + 0, + 0, + params, + 0, + mi_cmds, + 0, + 0, + 0, + 0, + mod_init, + 0, + destroy, + child_init, + 0 +}; + +static int mod_init(void) +{ + if (!otel_enabled) { + otel_enabled = (int *)shm_malloc(sizeof(*otel_enabled)); + if (!otel_enabled) { + LM_ERR("no shm memory for opentelemetry enable\n"); + return -1; + } + *otel_enabled = otel_enabled_cfg; + } + + if (!otel_is_enabled()) { + LM_INFO("opentelemetry module disabled\n"); + } + + if (otel_service_name.s) + otel_service_name.len = strlen(otel_service_name.s); + if (otel_exporter_endpoint.s) + otel_exporter_endpoint.len = strlen(otel_exporter_endpoint.s); + +#ifdef HAVE_OPENTELEMETRY_CPP + /* no provider init here; each process initializes on demand */ +#else + if (otel_is_enabled()) { + LM_ERR("OpenTelemetry C++ SDK not available - build with HAVE_OPENTELEMETRY_CPP\n"); + return -1; + } +#endif + + return 0; +} + +static int child_init(int rank) +{ + (void)rank; + + otel_span_reset(); + otel_log_in_cb = 0; + otel_parent_ctx_clear(); + +#ifdef HAVE_OPENTELEMETRY_CPP + if (!otel_trace_registered) { + if (register_route_tracer(&otel_trace_handlers) != 0) { + LM_ERR("failed to register route tracer hooks\n"); + return -1; + } + otel_trace_registered = 1; + } + + if (!otel_log_consumer_registered) { + if (register_log_consumer(OTEL_CONSUMER_NAME, otel_log_consumer, + otel_log_level, 0) != 0) { + LM_ERR("failed to register OpenTelemetry log consumer\n"); + return -1; + } + otel_log_consumer_registered = 1; + } + + if (otel_ensure_provider() != 0) + return -1; +#endif + + return 0; +} + +static mi_response_t *otel_mi_enable(const mi_params_t *params, + struct mi_handler *async_hdl) +{ + mi_response_t *resp; + mi_item_t *resp_obj; + int enable; + int rc; + + (void)async_hdl; + + if (get_mi_int_param(params, "enable", &enable) < 0) + return init_mi_param_error(); + + if (enable != 0 && enable != 1) + return init_mi_error(400, MI_SSTR("Bad enable value")); + + rc = otel_runtime_set_enable(enable); + if (rc < 0) + return init_mi_error(500, MI_SSTR("Failed to update enable")); + + resp = init_mi_result_object(&resp_obj); + if (!resp) + return 0; + if (add_mi_number(resp_obj, MI_SSTR("enabled"), enable) < 0) { + free_mi_response(resp); + return 0; + } + + return resp; +} + +static void destroy(void) +{ + if (otel_is_enabled()) + unregister_route_tracer(&otel_trace_handlers); + + otel_span_reset(); +} diff --git a/modules/tm/async.c b/modules/tm/async.c index 60354e7cbe..0f6600c40a 100644 --- a/modules/tm/async.c +++ b/modules/tm/async.c @@ -25,8 +25,11 @@ #include "../../dprint.h" #include "../../async.h" +#include "../../action.h" #include "../../context.h" #include "../../reactor_defs.h" +#include "../../route_trace.h" +#include #include "h_table.h" #include "t_lookup.h" #include "t_msgbuilder.h" @@ -36,6 +39,8 @@ typedef struct _async_tm_ctx { /* generic async context - MUST BE FIRST */ async_ctx async; + route_trace_ctx_t parent_ctx; + int parent_ctx_set; /* the script route to be used to continue after the resume function; * this is a reference in shm mem, that needs separated free */ struct script_route_ref *resume_route; @@ -93,6 +98,7 @@ int t_resume_async_request(int fd, void*param, int was_timeout) else LM_DBG("resuming request without a fd, transaction %p \n", t); + /* prepare for resume route, by filling in a phony UAC structure to * trigger the inheritance of the branch specific values */ uac.br_flags = getb0flags( t->uas.request ) ; @@ -186,6 +192,8 @@ int t_resume_async_request(int fd, void*param, int was_timeout) ctx->resume_route->name.s); } else { swap_route_type(route, ctx->route_type); + if (ctx->parent_ctx_set) + route_trace_set_ctx(&ctx->parent_ctx); run_resume_route( ctx->resume_route, &faked_req, 1); set_route_type(route); } @@ -237,6 +245,7 @@ int t_resume_async_reply(int fd, void*param, int was_timeout) LM_DBG("resuming reply without a fd, transaction %p \n", t); + /* enviroment setting */ current_processing_ctx = ctx->msg_ctx; backup_t = get_t(); @@ -332,6 +341,8 @@ int t_resume_async_reply(int fd, void*param, int was_timeout) ctx->resume_route->name.s); } else { swap_route_type(route, ctx->route_type); + if (ctx->parent_ctx_set) + route_trace_set_ctx(&ctx->parent_ctx); /* do not run any post script callback, we are a reply */ run_resume_route( ctx->resume_route, ctx->reply, 0); set_route_type(route); @@ -452,6 +463,22 @@ int t_handle_async(struct sip_msg *msg, struct action* a, memset(ctx,0,sizeof(async_tm_ctx)); ctx->async.timeout_s = timeout; + ctx->parent_ctx_set = route_trace_get_ctx(&ctx->parent_ctx); + if (ctx->parent_ctx_set) { + struct timespec ts; + int have_sys = 0; + int have_steady = 0; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + ctx->parent_ctx.start_system_ns = ((uint64_t)ts.tv_sec * 1000000000ULL) + (uint64_t)ts.tv_nsec; + have_sys = 1; + } + if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) { + ctx->parent_ctx.start_steady_ns = ((uint64_t)ts.tv_sec * 1000000000ULL) + (uint64_t)ts.tv_nsec; + have_steady = 1; + } + if (have_sys && have_steady) + ctx->parent_ctx.has_start_time = 1; + } async_status = ASYNC_NO_IO; /*assume default status "no IO done" */ return_code = ((const acmd_export_t*)(a->elem[0].u.data_const))->function(msg, @@ -608,4 +635,3 @@ int t_handle_async(struct sip_msg *msg, struct action* a, /* the triggering route is terminated and whole script ended */ return 0; } - diff --git a/route_trace.c b/route_trace.c new file mode 100644 index 0000000000..f8e69096ba --- /dev/null +++ b/route_trace.c @@ -0,0 +1,45 @@ +/* + * Route tracing hooks for external instrumentation + * + * Copyright (C) 2026 OpenSIPS Project + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * opensips 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "route_trace.h" +#include "dprint.h" + +route_trace_handlers_t *route_trace_handlers; + +int register_route_tracer(route_trace_handlers_t *handlers) +{ + if (!handlers) + return -1; + + if (route_trace_handlers && route_trace_handlers != handlers) { + LM_ERR("route tracer already registered\n"); + return -1; + } + + route_trace_handlers = handlers; + return 0; +} + +void unregister_route_tracer(route_trace_handlers_t *handlers) +{ + if (route_trace_handlers == handlers) + route_trace_handlers = NULL; +} diff --git a/route_trace.h b/route_trace.h new file mode 100644 index 0000000000..5b4dbd50e4 --- /dev/null +++ b/route_trace.h @@ -0,0 +1,112 @@ +/* + * Route tracing hooks for external instrumentation + * + * Copyright (C) 2026 OpenSIPS Project + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * opensips 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef ROUTE_TRACE_H +#define ROUTE_TRACE_H + +#include +#include + +struct sip_msg; + +typedef struct route_trace_ctx { + uint8_t trace_id[16]; + uint8_t span_id[8]; + uint8_t trace_flags; + uint64_t start_system_ns; + uint64_t start_steady_ns; + uint8_t has_start_time; +} route_trace_ctx_t; + +typedef struct route_trace_handlers { + void (*on_msg_start)(struct sip_msg *msg, int route_type, + const char *route_name, int stack_size, int stack_start); + void (*on_msg_end)(struct sip_msg *msg, int route_type, + const char *route_name, int stack_size, int stack_start, int status); + void (*on_route_enter)(struct sip_msg *msg, int route_type, + const char *route_name, const char *file, int line, + int stack_size, int stack_start); + void (*on_route_exit)(struct sip_msg *msg, int route_type, + const char *route_name, const char *file, int line, + int stack_size, int stack_start, int status); + int (*get_ctx)(route_trace_ctx_t *ctx); + int (*set_ctx)(const route_trace_ctx_t *ctx); +} route_trace_handlers_t; + +extern route_trace_handlers_t *route_trace_handlers; + +int register_route_tracer(route_trace_handlers_t *handlers); +void unregister_route_tracer(route_trace_handlers_t *handlers); + +static inline int route_trace_enabled(void) +{ + return route_trace_handlers != NULL; +} + +static inline void route_trace_msg_start(struct sip_msg *msg, int route_type, + const char *route_name, int stack_size, int stack_start) +{ + if (route_trace_handlers && route_trace_handlers->on_msg_start) + route_trace_handlers->on_msg_start(msg, route_type, route_name, + stack_size, stack_start); +} + +static inline void route_trace_msg_end(struct sip_msg *msg, int route_type, + const char *route_name, int stack_size, int stack_start, int status) +{ + if (route_trace_handlers && route_trace_handlers->on_msg_end) + route_trace_handlers->on_msg_end(msg, route_type, route_name, + stack_size, stack_start, status); +} + +static inline void route_trace_route_enter(struct sip_msg *msg, int route_type, + const char *route_name, const char *file, int line, + int stack_size, int stack_start) +{ + if (route_trace_handlers && route_trace_handlers->on_route_enter) + route_trace_handlers->on_route_enter(msg, route_type, route_name, + file, line, stack_size, stack_start); +} + +static inline void route_trace_route_exit(struct sip_msg *msg, int route_type, + const char *route_name, const char *file, int line, + int stack_size, int stack_start, int status) +{ + if (route_trace_handlers && route_trace_handlers->on_route_exit) + route_trace_handlers->on_route_exit(msg, route_type, route_name, + file, line, stack_size, stack_start, status); +} + +static inline int route_trace_get_ctx(route_trace_ctx_t *ctx) +{ + if (route_trace_handlers && route_trace_handlers->get_ctx) + return route_trace_handlers->get_ctx(ctx); + return 0; +} + +static inline int route_trace_set_ctx(const route_trace_ctx_t *ctx) +{ + if (route_trace_handlers && route_trace_handlers->set_ctx) + return route_trace_handlers->set_ctx(ctx); + return 0; +} + +#endif /* ROUTE_TRACE_H */