File: telemetry.cpp

package info (click to toggle)
pytango 10.1.4-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 8,304 kB
  • sloc: python: 27,795; cpp: 16,150; sql: 252; sh: 152; makefile: 43
file content (213 lines) | stat: -rw-r--r-- 8,297 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/*
 * SPDX-FileCopyrightText: All Contributors to the PyTango project
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

#include "common_header.h"
#include "convertors/type_casters.h"

#if defined(TANGO_USE_TELEMETRY)
// I.e., cppTango is compiled with telemetry support.

  #include <opentelemetry/sdk/common/global_log_handler.h>

namespace otel_log = opentelemetry::sdk::common::internal_log;

void set_log_level(std::string level_str) {
    std::transform(level_str.begin(), level_str.end(), level_str.begin(), ::toupper);

    static const std::unordered_map<std::string, otel_log::LogLevel> level_map = {
        {"NONE", otel_log::LogLevel::None},
        {"CRITICAL", otel_log::LogLevel::None},
        {"FATAL", otel_log::LogLevel::None},
        {"ERROR", otel_log::LogLevel::Error},
        {"WARNING", otel_log::LogLevel::Warning},
        {"INFO", otel_log::LogLevel::Info},
        {"DEBUG", otel_log::LogLevel::Debug}};

    auto it = level_map.find(level_str);
    if(it != level_map.end()) {
        otel_log::GlobalLogHandler::SetLogLevel(it->second);
    }
    // else ignore request, leaving default log level
}

Tango::telemetry::InterfacePtr telemetry_interface{nullptr};
bool shutdown{false};

void ensure_default_telemetry_interface_initialized() {
    if(shutdown) {
        return;
    }

    if(!telemetry_interface) {
        std::string client_name;
        if(Tango::ApiUtil::get_env_var("PYTANGO_TELEMETRY_CLIENT_SERVICE_NAME", client_name) != 0) {
            client_name = "pytango.client";
        }
        std::string name_space{"tango"};
        auto details = Tango::telemetry::Configuration::Client{client_name};
        Tango::telemetry::Configuration cfg{client_name, name_space, details};
        telemetry_interface = Tango::telemetry::InterfaceFactory::create(cfg);
    }
    // else: we already made our custom interface singleton.

    auto span = Tango::telemetry::Interface::get_current();
    if(span->is_default()) {
        // Make our client interface active (applies to current thread only, as cppTango uses a thread_local variable)
        Tango::telemetry::Interface::set_current(telemetry_interface);
    }
    // else: a non-default interface is either from a device, or we already set our client interface for this thread.
}

void cleanup_default_telemetry_interface() {
    // Ensure we release the telemetry interface object at shutdown time.  Hopefully, this happens before
    // OpenSSL's atexit handler starts cleaning up.  This is important if we are sending traces to an
    // https endpoint.  We need to flush any outstanding traces before shutting down
    shutdown = true;
    telemetry_interface = nullptr;
}

/*
 * Get the current trace context (from cppTango, to be used in PyTango).
 *
 * This function is used to propagate the trace context, fetching it from the cppTango kernel context,
 * The trace context is obtained in its W3C format as a dict of strings, with keys: "traceparent" and "tracestate".
 *
 * For details of the W3C format see: https://www.w3.org/TR/trace-context/
 */
py::dict get_trace_context() {
    ensure_default_telemetry_interface_initialized();

    std::string trace_parent;
    std::string trace_state;
    Tango::telemetry::Interface::get_trace_context(trace_parent, trace_state);

    py::dict carrier;
    carrier["traceparent"] = trace_parent;
    carrier["tracestate"] = trace_state;
    return carrier;
}

/*
 * Set the trace context (from PyTango to cppTango)
 *
 * This class is used to propagate trace context, writing the Python context into cppTango's telemetry context using
 * the two strings passed as constructor arguments (trace_parent & trace_state) in W3C format. A new span, with
 * the name specified by the "new_span_name" argument will be created when then acquire() method is called.
 * We have an acquire() method and a release() method so that this class can be used with a Python context handler.
 * Entering the context handler must call acquire(), which activates the scope in cppTango.  Exiting the context
 * handler must call release(), thus ending the scope (and associated span), and returning cppTango's context to
 * whatever it was before.  The restoration of the scope happens automatically when the scope pointer is released,
 * and the underlying cppTango object destroyed.
 *
 * For details of the W3C format see: https://www.w3.org/TR/trace-context/
 */
class TraceContextScope {
    Tango::telemetry::ScopePtr scope;
    const std::string new_span_name;
    std::string trace_parent;
    std::string trace_state;

  public:
    TraceContextScope(const std::string &new_span_name_,
                      const std::string &trace_parent_,
                      const std::string &trace_state_) :
        new_span_name{new_span_name_},
        trace_parent{trace_parent_},
        trace_state{trace_state_} {
    }

    void acquire() {
        if(scope == nullptr && !shutdown) {
            ensure_default_telemetry_interface_initialized();
            scope = Tango::telemetry::Interface::set_trace_context(
                new_span_name, trace_parent, trace_state, Tango::telemetry::Span::Kind::kClient);
        }
    }

    void release() {
        scope = nullptr;
    }

    ~TraceContextScope() {
        release();
    }
};

#else
// cppTango is *not* compiled with telemetry support.
// We use no-op handlers, so the Python code can run without errors but does nothing.

void no_op_cleanup() {
}

void no_op_set_log_level([[maybe_unused]] std::string level_str) { }

py::dict no_op_get_trace_context() {
    py::dict carrier;
    carrier["traceparent"] = "";
    carrier["tracestate"] = "";
    return carrier;
}

class NoOpTraceContextScope {
  public:
    NoOpTraceContextScope([[maybe_unused]] const std::string &new_span_name_,
                          [[maybe_unused]] const std::string &trace_parent_,
                          [[maybe_unused]] const std::string &trace_state_) {
    }

    void acquire() { }

    void release() { }

    ~NoOpTraceContextScope() { }
};

#endif

void export_telemetry_helpers(py::module_ &m) {
    py::module telemetry_module = m.def_submodule("_telemetry");

#if defined(TANGO_USE_TELEMETRY)
    telemetry_module.attr("TELEMETRY_ENABLED") = true;
    telemetry_module.def("get_trace_context", &get_trace_context);
    telemetry_module.def("cleanup_default_telemetry_interface", &cleanup_default_telemetry_interface);
    telemetry_module.def("set_log_level", &set_log_level);

    py::class_<TraceContextScope>(telemetry_module,
                                  "TraceContextScope",
                                  R"doc(
            Internal - for telemetry tracing purposes.

            Used to propagate the Python OpenTelemetry context to the cppTango telemetry context.
            When the context handler is entered, a new span is created.  During this process, the
            the current cppTango context is stored before the new span is set as the active scope.
            When the context handler exists, the span ends and the old context is restored at the
            C++ level.

            trace_parent and trace_state strings encoded as per the W3C standard: https://www.w3.org/TR/trace-context/

            with tango._telemetry.TraceContextScope(new_span_name, trace_parent, trace_state):
                x = proxy.read_attribute("foo")

            This is a no-op if telemetry support isn't compiled into cppTango (check tango.constants.TANGO_USE_TELEMETRY)

            .. versionadded:: 10.0.0)doc")
        .def(py::init<const std::string &, const std::string &, const std::string &>())
        .def("_acquire", &TraceContextScope::acquire)
        .def("_release", &TraceContextScope::release);
#else
    telemetry_module.attr("TELEMETRY_ENABLED") = false;
    telemetry_module.def("get_trace_context", &no_op_get_trace_context);
    telemetry_module.def("cleanup_default_telemetry_interface", &no_op_cleanup);
    telemetry_module.def("set_log_level", &no_op_set_log_level);

    py::class_<NoOpTraceContextScope>(telemetry_module, "TraceContextScope")
        .def(py::init<const std::string &, const std::string &, const std::string &>())
        .def("_acquire", &NoOpTraceContextScope::acquire)
        .def("_release", &NoOpTraceContextScope::release);
#endif
}