/*
 * SPDX-FileCopyrightText: 2025 Copyright contributors to the cppTango project
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */
#include <tango/common/telemetry/Interface.h>

#include <tango/internal/telemetry/InterfaceImplementation.h>
#include <tango/internal/telemetry/SpanImplementation.h>
#include <tango/internal/telemetry/StdUniqueToNostdShared.h>
#include <tango/internal/telemetry/TopicSampler.h>

#include <tango/client/ApiUtil.h>
#include <tango/common/telemetry/Span.h>
#include <tango/common/telemetry/TelemetryAppender.h>
#include <tango/common/utils/assert.h>
#include <tango/common/versions.h>
#include <tango/server/Util.h>

#include <mutex>

namespace Tango::telemetry
{

//-----------------------------------------------------------------------------------------
// helper function: to_opentelemetry_span_kind
//-----------------------------------------------------------------------------------------
inline static opentelemetry::trace::SpanKind to_opentelemetry_span_kind(const Span::Kind &kind)
{
    switch(kind)
    {
    case Span::Kind::kClient:
        return opentelemetry::trace::SpanKind::kClient;
    case Span::Kind::kServer:
        return opentelemetry::trace::SpanKind::kServer;
    case Span::Kind::kProducer:
        return opentelemetry::trace::SpanKind::kProducer;
    case Span::Kind::kConsumer:
        return opentelemetry::trace::SpanKind::kConsumer;
    case Span::Kind::kInternal:
    default:
        return opentelemetry::trace::SpanKind::kInternal;
    }
}

//-----------------------------------------------------------------------------------------
// TANGO CARRIER
//-----------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------
// TangoCarrier
//-----------------------------------------------------------------------------------------
// OpenTelemetry does not specify how the context is propagated. It simply provides a mechanism for
// injecting and extracting the context. This mechanism relies on a Propagator that itself delegates
// the actual I/O actions to a Carrier implementing a 'Set' (injection) and a 'Get' (extraction)
// method. We consequently have to provide a 'TangoCarrier' so that we will be able to inject/extract
// the the trace context from the data struct that carries it. So far, the context information is
// encapsulated into the ClntIdent data struct (of the CORBA IDL) passed by a client (the caller)
// to a server (the callee).
//-----------------------------------------------------------------------------------------
class TangoTextMapCarrier : public opentelemetry::context::propagation::TextMapCarrier
{
  public:
    TangoTextMapCarrier() = default;

    //-------------------------------------------------------------------------------------
    // Given a key, returns the associated value or and empty if there's no such key
    //-------------------------------------------------------------------------------------
    opentelemetry::nostd::string_view Get(opentelemetry::nostd::string_view key) const noexcept override
    {
        auto it = headers.find(std::string(key));
        if(it != headers.end())
        {
            return it->second;
        }
        return "";
    }

    //-------------------------------------------------------------------------------------
    // Given a key, sets its associated value
    //-------------------------------------------------------------------------------------
    void Set(opentelemetry::nostd::string_view key, opentelemetry::nostd::string_view value) noexcept override
    {
        headers.insert(std::pair<std::string, std::string>(std::string(key), std::string(value)));
    }

  private:
    std::map<std::string, std::string> headers;
};

//-----------------------------------------------------------------------------------------
// set_current_context: part of the trace context propagation - mutualizes the associated code
//-----------------------------------------------------------------------------------------
static opentelemetry::context::Context set_current_context(const std::string &trace_parent,
                                                           const std::string &trace_state) noexcept
{
    // inject the incoming W3C headers into a carrier
    TangoTextMapCarrier carrier;
    carrier.Set(opentelemetry::trace::propagation::kTraceParent, trace_parent);
    carrier.Set(opentelemetry::trace::propagation::kTraceState, trace_state);

    // get the current context
    auto current_context = opentelemetry::context::RuntimeContext::GetCurrent();

    // get the current propagator
    auto prop = opentelemetry::context::propagation::GlobalTextMapPropagator::GetGlobalPropagator();

    //--------------------------------------------------------------------------------------
    // breakdown of following call to prop->Extract:
    //----------------------------------------------
    // 1. the propagator to extract the new context from the carrier -> returns SpanContext from W3C headers
    // 2. the SpanContext is used to instantiate a DefaultSpan ->  returns a DefaultSpan (shared ptr) [1]
    // 3. the DefaultSpan is attached to the "current_context" (becomes the active one) -> returns a Context
    // 4. the Context is returned to the caller - will be used as the parent of the next downstream spans
    //--------------------------------------------------------------------------------------
    // [1] the class DefaultSpan provides a non-operational Span that propagates the tracer context by
    // wrapping it inside the Span object.
    //--------------------------------------------------------------------------------------
    return prop->Extract(carrier, current_context);
}

//-----------------------------------------------------------------------------------------
// Interface::get_current_context: return the current telemetry context
//-----------------------------------------------------------------------------------------
// this method is used by the Tango::Connection (and its child classes - e.g., DeviceProxy) to
// propagate the trace context to the callee. It could also be used by pyTango. The trace context
// is obtained in its W3C format trough the two strings passed as arguments.
//-----------------------------------------------------------------------------------------
static void get_current_context(std::string &trace_parent, std::string &trace_state) noexcept
{
    // ask the propagator to inject the current context in the specified carrier
    TangoTextMapCarrier carrier;
    auto context = opentelemetry::context::RuntimeContext::GetCurrent();
    auto prop = opentelemetry::context::propagation::GlobalTextMapPropagator::GetGlobalPropagator();
    prop->Inject(carrier, context);

    // extract the W3C headers from carrier
    trace_parent = std::string(carrier.Get(opentelemetry::trace::propagation::kTraceParent));
    trace_state = std::string(carrier.Get(opentelemetry::trace::propagation::kTraceState));
}

//-----------------------------------------------------------------------------------------
// InterfaceImplementation
//-----------------------------------------------------------------------------------------
void InterfaceImplementation::terminate() noexcept
{
    // flush traces
    cleanup_tracer_provider();
}

//-------------------------------------------------------------------------------------
// trace provider initialization
//-------------------------------------------------------------------------------------
void InterfaceImplementation::init_tracer_provider()
{
    // Don't engage with the telemetry system at all if not enabled
    if(!cfg.enabled)
    {
        return;
    }
    // see the following link for details on tracer naming:
    // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#get-a-tracer
    //- the tracer name is the 'instrumentation library' - here, it's simply the cpp version of tango
    std::string tracer_name = "tango.cpp";
    //- the tracer version is the cppTango version
    std::string tracer_version = git_revision();

    // Use a noop TracerProvider if tracing is disabled
    if(!cfg.tracing_enabled)
    {
        using TracerProviderPtr = opentelemetry::nostd::shared_ptr<opentelemetry::trace::TracerProvider>;
        provider = TracerProviderPtr{new opentelemetry::trace::NoopTracerProvider};
        tracer = provider->GetTracer(tracer_name, tracer_version);

        return;
    }

    // We always use a processor list, even if only one exporter is set
    std::vector<std::unique_ptr<opentelemetry::sdk::trace::SpanProcessor>> processorList;
    for(auto const &collector : cfg.get_tracing_collectors())
    {
        std::unique_ptr<opentelemetry::sdk::trace::SpanExporter> exporter;
        std::unique_ptr<opentelemetry::sdk::trace::SpanProcessor> processor;

        // we now have a valid endpoint for the given exporter type,
        // and we also already have checked the compiled features grpc and http for the requested exporter type
        switch(collector.exporter_type)
        {
        case Configuration::Exporter::grpc:
#ifdef TANGO_TELEMETRY_USE_GRPC
        {
            opentelemetry::exporter::otlp::OtlpGrpcExporterOptions opts;
            opts.endpoint = cfg.extract_grpc_host_port(collector.endpoint);
            opts.use_ssl_credentials = false;
            exporter = opentelemetry::exporter::otlp::OtlpGrpcExporterFactory::Create(opts);
        }
#endif
        break;
        case Configuration::Exporter::http:
#ifdef TANGO_TELEMETRY_USE_HTTP
        {
            opentelemetry::exporter::otlp::OtlpHttpExporterOptions opts;
            opts.url = collector.endpoint;
            exporter = opentelemetry::exporter::otlp::OtlpHttpExporterFactory::Create(opts);
        }
#endif
        break;
        case Configuration::Exporter::console:
            if(collector.endpoint == "cout")
            {
                exporter = opentelemetry::exporter::trace::OStreamSpanExporterFactory::Create(std::cout);
            }
            else if(collector.endpoint == "cerr")
            {
                exporter = opentelemetry::exporter::trace::OStreamSpanExporterFactory::Create(std::cerr);
            }
            else
            {
                TANGO_ASSERT(false);
            }
            break;
        default:
            TANGO_ASSERT_ON_DEFAULT(collector.exporter_type);
        }

        TANGO_ASSERT(exporter);

        switch(collector.exporter_type)
        {
        case Configuration::Exporter::grpc:
        case Configuration::Exporter::http:
        {
            opentelemetry::sdk::trace::BatchSpanProcessorOptions opts;
            opts.max_queue_size = cfg.max_batch_queue_size;
            opts.max_export_batch_size = cfg.traces_batch_size;
            opts.schedule_delay_millis = std::chrono::milliseconds(cfg.batch_schedule_delay_in_milliseconds);
            processor = opentelemetry::sdk::trace::BatchSpanProcessorFactory::Create(std::move(exporter), opts);
        }
        break;
        case Configuration::Exporter::console:
            // fix garbeled output with batch processing
            processor = opentelemetry::sdk::trace::SimpleSpanProcessorFactory::Create(std::move(exporter));
            break;
        default:
            TANGO_ASSERT_ON_DEFAULT(collector.exporter_type);
        }

        TANGO_ASSERT(processor);
        processorList.push_back(std::move(processor));
    }

    auto resource = opentelemetry::sdk::resource::Resource::Create(get_resource_attributes());

    /* The code below fails to compile with gcc 10.2.1
    using TracerProvider_SharedPtr = opentelemetry::nostd::shared_ptr<opentelemetry::sdk::trace::TracerProvider>;
    provider = TracerProvider_SharedPtr(opentelemetry::sdk::trace::TracerProviderFactory::Create(
        std::move(processorList), resource, std::make_unique<TopicSampler>(cfg)));
    */
    provider = StdUniqueToNostdShared(opentelemetry::sdk::trace::TracerProviderFactory::Create(
        std::move(processorList), resource, std::make_unique<TopicSampler>(cfg)));

    tracer = provider->GetTracer(tracer_name, tracer_version);
}

opentelemetry::sdk::resource::ResourceAttributes InterfaceImplementation::get_resource_attributes()
{
    Tango::Util *util{nullptr};
    try
    {
        util = Tango::Util::instance(false);
    }
    catch(...)
    {
        // ok, we are starting up... not a big deal
    }

    auto *api_util = Tango::ApiUtil::instance();

    std::string tango_host;
    api_util->get_env_var("TANGO_HOST", tango_host);

    opentelemetry::sdk::resource::ResourceAttributes resource_attributes;

    // check interface configuration kind
    if(cfg.is_a(Configuration::Kind::Server))
    {
        // interface is instantiated for a server
        const Configuration::Server &srv_info = std::get<0>(cfg.details);
        resource_attributes = opentelemetry::sdk::resource::ResourceAttributes{
            {"service.namespace", cfg.name_space.empty() ? "tango" : cfg.name_space},
            {"service.name", srv_info.class_name},         // naming convention of OpenTelemetry
            {"service.instance.id", srv_info.device_name}, // naming convention of OpenTelemetry
            {"tango.server.name",
             util != nullptr ? util->get_ds_exec_name() + "/" + util->get_ds_inst_name() : "unknown"},
            {"tango.process.id", api_util->get_client_pid()},
            {"tango.process.kind", api_util->in_server() ? "server" : "client"}};
        // Only set tango.host if the DB is being used
        if(util->use_db() && !util->use_file_db())
        {
            resource_attributes.SetAttribute("tango.host", tango_host);
        }
    }
    else
    {
        // interface is instantiated for a client
        const Configuration::Client &clt_info = std::get<1>(cfg.details);
        resource_attributes = opentelemetry::sdk::resource::ResourceAttributes{
            {"service.namespace", cfg.name_space.empty() ? "tango" : cfg.name_space},
            {"service.name", clt_info.name}, // naming convention of OpenTelemetry
            {"tango.process.id", api_util->get_client_pid()},
            {"tango.process.kind", api_util->in_server() ? "server" : "client"},
            {"tango.host", tango_host}};
    }

    return resource_attributes;
}

void InterfaceImplementation::cleanup_tracer_provider() noexcept
{
    if(provider)
    {
        auto *base_class = dynamic_cast<opentelemetry::sdk::trace::TracerProvider *>(provider.get());
        if(base_class != nullptr)
        {
            base_class->ForceFlush();
        }
    }
}

void InterfaceImplementation::init_global_propagator() noexcept
{
    const std::scoped_lock lock(InterfaceImplementation::global_propagator_initialized_mutex);

    // no mutex need cause devices are created sequentially at startup and the first device is created
    // there's no more danger to have a race condition on the global_propagator_initialized
    if(!InterfaceImplementation::global_propagator_initialized)
    {
        opentelemetry::context::propagation::GlobalTextMapPropagator::SetGlobalPropagator(
            opentelemetry::nostd::shared_ptr<opentelemetry::context::propagation::TextMapPropagator>(
                new(std::nothrow) opentelemetry::trace::propagation::HttpTraceContext()));

        InterfaceImplementation::global_propagator_initialized = true;
    }
}

TracerPtr InterfaceImplementation::get_tracer() const noexcept
{
    return tracer;
}

SpanPtr InterfaceImplementation::instantiate_span(
    opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span> otel_span) const noexcept
{
    //----------------------------------------------------------------------------------------
    // the following code could look weird but it allows to obtain a Span class that can not be instantiated
    // externally (i.e., from the user space).
    //----------------------------------------------------------------------------------------
    // step-1: instantiate a span without any impl.
    // here we follow the iso-cpp guidelines: don't deal with bad_alloc cause nobody can recover from it! see:
    // https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#f6-if-your-function-must-not-throw-declare-it-noexcept
    auto *span = new(std::nothrow) Span();
    // step-2: instantiate the impl then attach it to the span
    span->impl = std::make_unique<SpanImplementation>(std::move(otel_span));
    // step-3: return the new span
    return std::unique_ptr<Span>(span);
}

SpanPtr InterfaceImplementation::start_span(const std::string &name,
                                            const Attributes &attributes,
                                            const Span::Kind &kind) noexcept
{
    std::map<std::string, opentelemetry::common::AttributeValue> otel_attributes;

    for(const auto &attribute : attributes)
    {
        std::visit(
            [&](auto &&arg)
            {
                // Convert each type to opentelemetry::trace::AttributeValue and add to convertedAttributes
                otel_attributes[attribute.first] = opentelemetry::common::AttributeValue(arg);
            },
            attribute.second);
    }

    auto otel_span = get_tracer()->StartSpan(
        name,
        otel_attributes,
        {{}, {}, opentelemetry::trace::SpanContext(false, false), to_opentelemetry_span_kind(kind)});

    return instantiate_span(otel_span);
}

//-------------------------------------------------------------------------------------
// start_span
//-------------------------------------------------------------------------------------
SpanPtr InterfaceImplementation::start_span(const std::string &name,
                                            const Attributes &attributes,
                                            const opentelemetry::trace::StartSpanOptions &options) noexcept
{
    std::map<std::string, opentelemetry::common::AttributeValue> otel_attributes;

    for(const auto &attribute : attributes)
    {
        std::visit(
            [&](auto &&arg)
            {
                // Convert each type to opentelemetry::trace::AttributeValue and add to convertedAttributes
                otel_attributes[attribute.first] = opentelemetry::common::AttributeValue(arg);
            },
            attribute.second);
    }

    auto otel_span = get_tracer()->StartSpan(name, otel_attributes, options);

    return instantiate_span(otel_span);
}

//-------------------------------------------------------------------------------------
// get_current_span
//-------------------------------------------------------------------------------------
SpanPtr InterfaceImplementation::get_current_span() const noexcept

{
    auto current_otel_span = get_tracer()->GetCurrentSpan();

    return instantiate_span(current_otel_span);
}

//-------------------------------------------------------------------------------------
// get_current_opentelemetry_span
//-------------------------------------------------------------------------------------
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
    InterfaceImplementation::get_current_opentelemetry_span() noexcept

{
    return get_tracer()->GetCurrentSpan();
}

//-----------------------------------------------------------------------------------------
// The telemetry::Interface currently attached to the current thread  (thread_local - TSS)
//-----------------------------------------------------------------------------------------
thread_local InterfacePtr current_telemetry_interface{nullptr};

//-----------------------------------------------------------------------------------------
// The default telemetry Interface - used in case none is attached to the current thread
//-----------------------------------------------------------------------------------------
Tango::telemetry::InterfacePtr InterfaceImplementation::default_telemetry_interface{nullptr};

//-----------------------------------------------------------------------------------------
// The mutex protecting the instantiation of the default interface against race conditions
//-----------------------------------------------------------------------------------------
std::mutex InterfaceImplementation::default_telemetry_interface_mutex;

//-----------------------------------------------------------------------------------------
// The "global_propagator_initialized" flag
//-----------------------------------------------------------------------------------------
bool InterfaceImplementation::global_propagator_initialized{false};

//-----------------------------------------------------------------------------------------
// The mutex protecting the instantiation of the global propagator against race conditions
//-----------------------------------------------------------------------------------------
std::mutex InterfaceImplementation::global_propagator_initialized_mutex;

//-----------------------------------------------------------------------------------------
// A pointer to an InterfaceImplementation
//-----------------------------------------------------------------------------------------
using InterfaceImplementationPtr = std::shared_ptr<InterfaceImplementation>;

//-----------------------------------------------------------------------------------------
// Interface::Interface
//-----------------------------------------------------------------------------------------
Interface::Interface(const Configuration &config)
{
    impl = std::make_shared<InterfaceImplementation>(config);
}

//-----------------------------------------------------------------------------------------
// Interface::~Interface
//-----------------------------------------------------------------------------------------
Interface::~Interface()
{
    impl->terminate();
}

//-----------------------------------------------------------------------------------------
//! Interface::get_configuration
//-----------------------------------------------------------------------------------------
const Configuration &Interface::get_configuration() const noexcept
{
    return impl->cfg;
}

//-----------------------------------------------------------------------------------------
//! Interface::get_appender
//-----------------------------------------------------------------------------------------
log4tango::Appender *Interface::get_appender() const noexcept
{
    return new(std::nothrow) TelemetryAppender(impl);
}

//-----------------------------------------------------------------------------------------
// Interface::is_enabled
//-----------------------------------------------------------------------------------------
bool Interface::is_enabled() const noexcept
{
    return impl->cfg.enabled;
}

//-----------------------------------------------------------------------------------------
// Interface::enable
//-----------------------------------------------------------------------------------------
void Interface::enable() noexcept
{
    if(!impl->cfg.enabled)
    {
        impl->cfg.enabled = true;
        impl->init_tracer_provider();
    }
}

//-----------------------------------------------------------------------------------------
// Interface::disable
//-----------------------------------------------------------------------------------------
void Interface::disable() noexcept
{
    if(impl->cfg.enabled)
    {
        impl->terminate();
        impl->cfg.enabled = false;
    }
}

//-----------------------------------------------------------------------------------------
// Interface::set_topics (vector)
//-----------------------------------------------------------------------------------------
void Interface::set_topics(const std::vector<std::string> &topics)
{
    impl->terminate();
    impl->cfg.set_topics(topics);
    impl->init_tracer_provider();
}

//-----------------------------------------------------------------------------------------
// Interface::set_topics (csv string)
//-----------------------------------------------------------------------------------------
void Interface::set_topics(const std::string &topics_str)
{
    impl->terminate();
    impl->cfg.set_topics(topics_str);
    impl->init_tracer_provider();
}

//-----------------------------------------------------------------------------------------
// Interface::get_topics
//-----------------------------------------------------------------------------------------
std::vector<std::string> Interface::get_topics()
{
    return impl->cfg.get_topics();
}

//-----------------------------------------------------------------------------------------
// Interface::set_tracing
//-----------------------------------------------------------------------------------------
void Interface::set_tracing(bool enable_flag) noexcept
{
    if(impl->cfg.tracing_enabled != enable_flag)
    {
        if(!enable_flag)
        {
            impl->terminate();
        }
        impl->cfg.tracing_enabled = enable_flag;
        impl->init_tracer_provider();
    }
}

//-----------------------------------------------------------------------------------------
// Interface::is_tracing_enabled
//-----------------------------------------------------------------------------------------
bool Interface::is_tracing_enabled() noexcept
{
    return impl->cfg.tracing_enabled;
}

//-----------------------------------------------------------------------------------------
// Interface::update_tracing_configuration
// ----------------------------------------------------------------------------------------
void Interface::update_tracing_configuration(Configuration newcfg) noexcept
{
    impl->terminate();
    impl->cfg = newcfg;
    impl->init_tracer_provider();
}

//-----------------------------------------------------------------------------------------
// Interface::update_configuration -- update without stopping/restarting telemetry
// ----------------------------------------------------------------------------------------
void Interface::update_configuration(Configuration newcfg) noexcept
{
    impl->cfg = newcfg;
}

//-----------------------------------------------------------------------------------------
// Interface::is_logging_enabled
//-----------------------------------------------------------------------------------------
bool Interface::is_logging_enabled() noexcept
{
    return impl->cfg.logging_enabled;
}

//-----------------------------------------------------------------------------------------
// Interface::get_id
//-----------------------------------------------------------------------------------------
const std::string &Interface::get_id() const noexcept
{
    return impl->cfg.id;
}

//-----------------------------------------------------------------------------------------
//  Interface::start_span
//-----------------------------------------------------------------------------------------
SpanPtr Interface::start_span(const std::string &name, const Attributes &attributes, const Span::Kind &kind) noexcept
{
    if(!is_enabled())
    {
        return nullptr;
    }
    return impl->start_span(name, attributes, kind);
}

//-----------------------------------------------------------------------------------------
//  Interface::get_current_span
//-----------------------------------------------------------------------------------------
SpanPtr Interface::get_current_span() const noexcept
{
    if(!is_enabled())
    {
        return nullptr;
    }
    return impl->get_current_span();
}

//-----------------------------------------------------------------------------------------
// Interface::set_trace_context
//-----------------------------------------------------------------------------------------
// set_trace_context: context propagation. This method is used by the Tango::Device_[X]Impl to
// setup the trace context upon receipt of a remote call. This flavor of "set_trace_context" is
// used where the caller is using an IDL version >= 4 and propagates context information.
//-----------------------------------------------------------------------------------------
ScopePtr Interface::set_trace_context(const std::string &new_span_name,
                                      Tango::telemetry::Attributes span_attr,
                                      const Tango::ClntIdent &client_identification,
                                      const char *client_ip) noexcept
{
    //  get telemetry interface attached to the current thread
    auto current_interface = Tango::telemetry::Interface::get_current();
    if(!current_interface->is_enabled())
    {
        return nullptr;
    }

    Tango::LockerLanguage cl_lang = client_identification._d();

    const TraceContext *trace_context{nullptr};
    switch(cl_lang)
    {
    case Tango::JAVA:
    {
        std::stringstream ident_stream;
        ident_stream << std::hex << client_identification.java_clnt().uuid[0];
        ident_stream << std::hex << client_identification.java_clnt().uuid[1];
        std::string java_class{client_identification.java_clnt().MainClass};
        span_attr["tango.client_ident.java_ident"] = ident_stream.str();
        span_attr["tango.client_ident.java_class"] = java_class;
        span_attr["tango.client_ident.pid"] = java_class.erase(0, java_class.find("PID=") + 4);
        span_attr["tango.client_ident.lang"] = "JAVA";
        span_attr["tango.client_ident.location"] = client_ip;
    }
    break;
    case Tango::JAVA_6:
    {
        trace_context = &client_identification.java_clnt_6().trace_context;

        std::stringstream ident_stream;
        ident_stream << std::hex << client_identification.java_clnt_6().java_clnt.uuid[0];
        ident_stream << std::hex << client_identification.java_clnt_6().java_clnt.uuid[1];
        std::string java_class{client_identification.java_clnt().MainClass};
        span_attr["tango.client_ident.java_ident"] = ident_stream.str();
        span_attr["tango.client_ident.java_class"] = java_class;
        span_attr["tango.client_ident.pid"] = java_class.erase(0, java_class.find("PID=") + 4);
        span_attr["tango.client_ident.lang"] = "JAVA";
        span_attr["tango.client_ident.location"] = client_ip;
    }
    break;
    case Tango::CPP_6:
        trace_context = &client_identification.cpp_clnt_6().trace_context;

        span_attr["tango.client_ident.lang"] = "CPP";
        span_attr["tango.client_ident.pid"] = static_cast<unsigned int>(client_identification.cpp_clnt_6().cpp_clnt);
        span_attr["tango.client_ident.location"] = client_ip;
        break;
    case Tango::CPP:
        span_attr["tango.client_ident.lang"] = "CPP";
        span_attr["tango.client_ident.pid"] = static_cast<unsigned int>(client_identification.cpp_clnt());
        span_attr["tango.client_ident.location"] = client_ip;
        break;
    default:
        break;
    }

    if(cl_lang == Tango::CPP || cl_lang == Tango::JAVA)
    {
        return std::make_unique<Scope>(current_interface->start_span(new_span_name, span_attr));
    }

    // telemetry: get trace context from ClntIdent
    switch(cl_lang)
    {
    case Tango::CPP_6:
    {
        trace_context = &client_identification.cpp_clnt_6().trace_context;
    }
    break;
    case Tango::JAVA_6:
    {
        trace_context = &client_identification.java_clnt_6().trace_context;
    }
    break;
    default:
        // unknown IDL version: simply ignore it till someone upgrade the code
        break;
    }

    if(trace_context == nullptr)
    {
        // weird case...
        // deal with that just as if we didn't receive any trace context info from the client
        return std::make_unique<Scope>(current_interface->start_span(new_span_name, span_attr));
    }

    // this is what we wnat to extract - see W3C trace context standard for info
    std::string trace_parent;
    std::string trace_state;

    switch(trace_context->_d())
    {
    case Tango::W3C_TC_V0:
    default:
    {
        const Tango::W3CTraceContextV0 &tc_data = trace_context->data();
        trace_parent = tc_data.trace_parent;
        trace_state = tc_data.trace_state;
    }
    }

    // make the incoming context the current "local" one (see set_current_context from details)
    auto new_context = set_current_context(trace_parent, trace_state);

    // create the server counterpart of the incoming client trace (i.e., make the link between caller and callee)
    opentelemetry::trace::StartSpanOptions options;
    // the active DefaultSpan of the "new_context" becomes the parent of the downstream spans (see set_current_context)
    options.parent = opentelemetry::trace::GetSpan(new_context)->GetContext();
    // make sure we associated the server span to the client span one (server counterpart - that is critical)
    options.kind = opentelemetry::trace::SpanKind::kServer;

    // ok, let's create and return the "local root span" of the distributed transaction in progress
    return std::make_unique<Scope>(current_interface->impl->start_span(new_span_name, span_attr, options));
}

//-----------------------------------------------------------------------------------------
// Interface::set_trace_context
//-----------------------------------------------------------------------------------------
// set_trace_context: context propagation. This method is used by the Tango::Device_[X]Impl to
// setup the trace context upon receipt of a remote call. This flavor of "set_trace_context" is
// used where the caller is using an IDL version < 4 and do not propagate any context information.
//-----------------------------------------------------------------------------------------
ScopePtr Interface::set_trace_context(const std::string &new_span_name,
                                      const Tango::telemetry::Attributes &span_attr) noexcept
{
    //  get telemetry interface attached to the current thread
    auto current_interface = Tango::telemetry::Interface::get_current();
    if(!current_interface->is_enabled())
    {
        return nullptr;
    }
    // no trace context to propagate
    opentelemetry::trace::StartSpanOptions options;
    options.kind = opentelemetry::trace::SpanKind::kServer;
    return std::make_unique<Scope>(current_interface->impl->start_span(new_span_name, span_attr, options));
}

//-----------------------------------------------------------------------------------------
// Interface::set_trace_context
//-----------------------------------------------------------------------------------------
// this is a helper function for pyTango: the python binding use the python native implementation
// of OpenTelemetry. All we have to do is to provide it with a way to get and set the current
// telemetry context when it call the kernel back (e.g., making use of a DeviceProxy). This
// method allows to set the current telemetry context (i.e., to propagate the python context to
// c++) using its W3C format trough the two strings passed as arguments.
//-----------------------------------------------------------------------------------------
ScopePtr Interface::set_trace_context(const std::string &new_span_name,
                                      const std::string &trace_parent,
                                      const std::string &trace_state,
                                      const Span::Kind &kind) noexcept
{
    //  get telemetry interface attached to the current thread
    auto current_interface = Tango::telemetry::Interface::get_current();
    if(!current_interface->is_enabled())
    {
        return nullptr;
    }
    // make the incoming context the current "local" one (see set_current_context from details)
    auto new_context = set_current_context(trace_parent, trace_state);

    // create the server counterpart of the incoming client trace (i.e., make the link between caller and callee)
    opentelemetry::trace::StartSpanOptions options;
    // the active DefaultSpan of the "new_context" becomes the parent of the downstream spans (see set_current_context)
    options.parent = opentelemetry::trace::GetSpan(new_context)->GetContext();
    // make sure we associated the server span to the client span one (server counterpart)
    options.kind = to_opentelemetry_span_kind(kind);

    // ok, let's create and return the "local root span" of the distributed transaction in progress
    return std::make_unique<Scope>(current_interface->impl->start_span(new_span_name, {}, options));
}

//-----------------------------------------------------------------------------------------
// Interface::get_current_context: return the current telemetry context
//-----------------------------------------------------------------------------------------
// this method is used by the Tango::Connection (and its child classes - e.g., DeviceProxy) to
// propagate the trace context to the callee. It could also be used by pyTango. The trace context
// is obtained in its W3C format trough the two strings passed as arguments.
//-----------------------------------------------------------------------------------------
void Interface::get_trace_context(std::string &trace_parent, std::string &trace_state) noexcept
{
    // get_current_context uses the Otel telemetry system, so only attempt if TTS is enabled
    auto current_interface = Tango::telemetry::Interface::get_current();
    if(!current_interface->is_enabled())
    {
        return;
    }
    get_current_context(trace_parent, trace_state);
}

//-----------------------------------------------------------------------------------------
// Interface::get_default_interface: returns the default Tango::telemetry::Interface
//-----------------------------------------------------------------------------------------
Tango::telemetry::InterfacePtr Interface::get_default_interface()
{
    const std::scoped_lock lock(InterfaceImplementation::default_telemetry_interface_mutex);

    if(!InterfaceImplementation::default_telemetry_interface)
    {
        InterfaceImplementation::default_telemetry_interface =
            std::make_shared<Tango::telemetry::Interface>(Configuration{});

        // mark this interface as the default one
        InterfaceImplementation::default_telemetry_interface->impl->is_default_interface = true;
    }

    return InterfaceImplementation::default_telemetry_interface;
}

//-----------------------------------------------------------------------------------------
// Interface::is_default:
// returns true if the telemetry interface the default one, returns false otherwise.
//-----------------------------------------------------------------------------------------
bool Interface::is_default() const noexcept
{
    return impl->is_default_interface;
}

//-----------------------------------------------------------------------------------------
// Interface::trace_exception:
// a helper function that tries to extract an err. msg. from the current exception
//-----------------------------------------------------------------------------------------
std::string Interface::extract_exception_info(std::exception_ptr current_exception)
{
    // be sure we have a validate input
    if(!current_exception)
    {
        return "unknown exception caught (no details available)";
    }

    // identify the exception by rethrowing it
    std::stringstream err;
    try
    {
        std::rethrow_exception(current_exception);
    }
    catch(const Tango::DevFailed &tango_ex)
    {
        if(tango_ex.errors.length() > 0)
        {
            err << "EXCEPTION:Tango::DevFailed;REASON:" << tango_ex.errors[0].reason
                << ";DESC:" << tango_ex.errors[0].desc << ";ORIGIN:" << tango_ex.errors[0].origin;
        }
        else
        {
            err << "EXCEPTION:Tango::DevFailed;REASON:unknown;DESC:unknown;ORIGIN:unknown";
        }
    }
    catch(const std::exception &std_ex)
    {
        err << "EXCEPTION:std::exception;DESC:unknown" << std_ex.what();
    }
    catch(...)
    {
        err << "EXCEPTION:unknown;DESC:unknown";
    }

    // return exception info (err msg)...
    return err.str();
}

//-----------------------------------------------------------------------------------------
// Interface::extract_exception_info:
// a helper function that tries to extract an err. msg. from the current exception
//-----------------------------------------------------------------------------------------
void Interface::extract_exception_info(std::string &type, std::string &message)
{
    std::exception_ptr current_exception = std::current_exception();

    // be sure we have a validate input
    if(!current_exception)
    {
        type = "unknown";
        message = "there is currently no exception";
        return;
    }

    // identify the exception by rethrowing it
    try
    {
        std::rethrow_exception(current_exception);
    }
    catch(const Tango::DevFailed &tango_ex)
    {
        type = "Tango::DevFailed";
        std::stringstream err;
        if(tango_ex.errors.length() > 0)
        {
            err << "REASON:" << tango_ex.errors[0].reason << ";DESC:" << tango_ex.errors[0].desc
                << ";ORIGIN:" << tango_ex.errors[0].origin;
        }
        else
        {
            err << "REASON:unknown;DESC:unknown;ORIGIN:unknown";
        }
        message = err.str();
    }
    catch(const std::exception &std_ex)
    {
        type = "std::exception";
        message = std_ex.what();
    }
    catch(...)
    {
        type = "unknown";
        message = "unknown exception caught";
    }
}

} // namespace Tango::telemetry
