345345-- Plugin span management functions
346346-- =================================
347347
348+ -- Build a consistent key for identifying a plugin phase span
349+ local function build_plugin_phase_key (plugin_name , phase )
350+ return plugin_name .. " :" .. phase
351+ end
352+
348353-- Create phase span
349354local function create_phase_span (api_ctx , plugin_name , phase )
350355 if not api_ctx .otel then
@@ -356,7 +361,7 @@ local function create_phase_span(api_ctx, plugin_name, phase)
356361 end
357362
358363 -- Create unique key for plugin+phase combination
359- local span_key = plugin_name .. " : " .. phase
364+ local span_key = build_plugin_phase_key ( plugin_name , phase )
360365 if not api_ctx .otel_plugin_spans [span_key ] then
361366 -- Create span named "plugin_name phase" directly under main request span
362367 local phase_span_ctx = api_ctx .otel .start_span ({
@@ -382,7 +387,7 @@ local function finish_phase_span(api_ctx, plugin_name, phase, error_msg)
382387 return
383388 end
384389
385- local span_key = plugin_name .. " : " .. phase
390+ local span_key = build_plugin_phase_key ( plugin_name , phase )
386391 local phase_span_ctx = api_ctx .otel_plugin_spans [span_key ]
387392
388393 if phase_span_ctx then
@@ -404,7 +409,7 @@ local function cleanup_plugin_spans(api_ctx)
404409
405410 for span_key , phase_span_ctx in pairs (api_ctx .otel_plugin_spans ) do
406411 if phase_span_ctx then
407- api_ctx .otel .stop_span (phase_span_ctx , " plugin span cleanup: check logs for details " )
412+ api_ctx .otel .stop_span (phase_span_ctx )
408413 end
409414 end
410415
@@ -417,23 +422,21 @@ end
417422-- =============================
418423
419424-- No-op API when tracing is disabled
420- local noop_api = {
421- start_span = function (span_info )
422- return nil
423- end ,
424-
425- stop_span = function (span_ctx , error_msg )
426- -- no-op
427- end ,
428-
429- current_span = function ()
430- return nil
431- end ,
432-
433- get_plugin_context = function (plugin_name )
434- return nil
425+ local noop_api = setmetatable ({
426+ with_span = function (span_info , fn )
427+ if not fn then
428+ return nil , " with_span: function is required"
429+ end
430+ -- Execute function without tracing
431+ local result = {pcall (fn )}
432+ -- Return unpacked results (starting from index 2 to preserve error-first pattern)
433+ return unpack (result , 2 )
435434 end
436- }
435+ }, {
436+ __index = function (_ , _ )
437+ return function () return nil end
438+ end
439+ })
437440
438441-- Create simple OpenTelemetry API for plugins
439442local function create_otel_api (api_ctx , tracer , main_context )
@@ -442,56 +445,60 @@ local function create_otel_api(api_ctx, tracer, main_context)
442445 api_ctx ._otel_span_stack = {}
443446 end
444447
445- return {
446- start_span = function (span_info )
447- if not (span_info and span_info .name ) then
448- return nil
449- end
448+ local function start_span_func (span_info )
449+ if not (span_info and span_info .name ) then
450+ return nil
451+ end
450452
451- -- Get parent context (prioritize explicit parent, then current phase span, then main)
452- local current_phase_span = api_ctx ._current_plugin_phase and
453- api_ctx .otel_plugin_spans and
454- api_ctx .otel_plugin_spans [api_ctx ._current_plugin_phase ]
453+ -- Get parent context (prioritize explicit parent, then current phase span, then main)
454+ local current_phase_span = api_ctx ._current_plugin_phase and
455+ api_ctx .otel_plugin_spans and
456+ api_ctx .otel_plugin_spans [api_ctx ._current_plugin_phase ]
455457
456- local parent_context = span_info .parent or current_phase_span or main_context
458+ local parent_context = span_info .parent or current_phase_span or main_context
457459
458- -- Use the provided kind directly (users should pass span_kind constants)
459- local span_kind_value = span_info .kind or span_kind .internal
460- local attributes = span_info .attributes or {}
461- local span_ctx = tracer :start (parent_context , span_info .name , {
462- kind = span_kind_value ,
463- attributes = attributes ,
464- })
460+ -- Use the provided kind directly (users should pass span_kind constants)
461+ local span_kind_value = span_info .kind or span_kind .internal
462+ local attributes = span_info .attributes or {}
463+ local span_ctx = tracer :start (parent_context , span_info .name , {
464+ kind = span_kind_value ,
465+ attributes = attributes ,
466+ })
465467
466- -- Track this span as current (push to stack)
467- core .table .insert (api_ctx ._otel_span_stack , span_ctx )
468+ -- Track this span as current (push to stack)
469+ core .table .insert (api_ctx ._otel_span_stack , span_ctx )
468470
469- return span_ctx
470- end ,
471+ return span_ctx
472+ end
471473
472- stop_span = function (span_ctx , error_msg )
473- if not span_ctx then
474- return
475- end
474+ local function stop_span_func (span_ctx , error_msg )
475+ if not span_ctx then
476+ return
477+ end
476478
477- local span = span_ctx :span ()
478- if not span then
479- return
480- end
479+ local span = span_ctx :span ()
480+ if not span then
481+ return
482+ end
481483
482- if error_msg then
483- span :set_status (span_status .ERROR , error_msg )
484- end
484+ if error_msg then
485+ span :set_status (span_status .ERROR , error_msg )
486+ end
485487
486- span :finish ()
488+ span :finish ()
487489
488- -- Remove from stack if it's the current span (pop from stack)
489- if api_ctx ._otel_span_stack and
490- # api_ctx ._otel_span_stack > 0 and
491- api_ctx ._otel_span_stack [# api_ctx ._otel_span_stack ] == span_ctx then
492- core .table .remove (api_ctx ._otel_span_stack )
493- end
494- end ,
490+ -- Remove from stack if it's the current span (pop from stack)
491+ if api_ctx ._otel_span_stack and
492+ # api_ctx ._otel_span_stack > 0 and
493+ api_ctx ._otel_span_stack [# api_ctx ._otel_span_stack ] == span_ctx then
494+ core .table .remove (api_ctx ._otel_span_stack )
495+ end
496+ end
497+
498+ return {
499+ start_span = start_span_func ,
500+
501+ stop_span = stop_span_func ,
495502
496503 current_span = function ()
497504 -- Return the most recently started span (top of stack)
@@ -505,7 +512,43 @@ local function create_otel_api(api_ctx, tracer, main_context)
505512 if not (api_ctx .otel_plugin_spans and phase ) then
506513 return nil
507514 end
508- return api_ctx .otel_plugin_spans [plugin_name .. " :" .. phase ]
515+ return api_ctx .otel_plugin_spans [build_plugin_phase_key (plugin_name , phase )]
516+ end ,
517+
518+ with_span = function (span_info , fn )
519+ if not fn then
520+ return nil , " with_span: function is required"
521+ end
522+ -- Start the span
523+ local span_ctx = start_span_func (span_info )
524+ if not span_ctx then
525+ -- If span creation fails, still execute the function without tracing
526+ local result = {pcall (fn )}
527+ return unpack (result , 2 )
528+ end
529+ -- Execute function with pcall for error protection
530+ local result = {pcall (fn )}
531+ -- Handle results:
532+ -- - If pcall fails: result[1] = false, result[2] = Lua error
533+ -- - If function succeeds: result[1] = true, result[2] = err (from function), result[3+] = other values
534+ local pcall_success , error_msg = result [1 ], result [2 ]
535+ -- Determine the actual error to report:
536+ -- - If pcall failed, use the Lua error
537+ -- - If pcall succeeded but function returned an error, use the function error
538+ -- - Otherwise, no error
539+ local final_error = nil
540+ if not pcall_success then
541+ -- pcall failed - Lua error occurred
542+ final_error = error_msg
543+ elseif error_msg ~= nil then
544+ -- pcall succeeded but function returned an error
545+ final_error = error_msg
546+ end
547+ -- Stop span with error message if there was an error
548+ stop_span_func (span_ctx , final_error )
549+ -- Return unpacked results (starting from index 2 to preserve error-first pattern)
550+ -- This returns: err, ...values
551+ return unpack (result , 2 )
509552 end
510553 }
511554end
0 commit comments