diff --git a/examples/zpages/BUILD b/examples/zpages/BUILD index 9811ddce23..52703f11f0 100644 --- a/examples/zpages/BUILD +++ b/examples/zpages/BUILD @@ -29,3 +29,20 @@ cc_binary( "//ext/src/zpages", ], ) + + +cc_binary( + name = "zpagesexample", + srcs = [ + "zpagesexample.cc" + ], + linkopts = select({ + "//bazel:windows": [], + "//conditions:default": ["-pthread"], + }), + deps = [ + "//ext:headers", + "//sdk/src/trace", + "//ext/src/zpages", + ], +) diff --git a/examples/zpages/zpages_example.cc b/examples/zpages/zpages_example.cc index 0ddd0dfdbd..2d6d36c37f 100644 --- a/examples/zpages/zpages_example.cc +++ b/examples/zpages/zpages_example.cc @@ -1,61 +1,48 @@ -#pragma once - -#include +/** + * This is a basic example for zpages that helps users get familiar with how to + * use this feature in OpenTelemetery + */ #include #include #include -#include -#include "opentelemetry/ext/zpages/zpages.h" - -using opentelemetry::core::SteadyTimestamp; -namespace nostd = opentelemetry::nostd; -using opentelemetry::v0::trace::Span; +#include "opentelemetry/ext/zpages/zpages.h" // Required file include for zpages int main(int argc, char* argv[]) { + + /** + * The following line initializes zPages and starts a webserver at + * http://localhost:30000/tracez/ where spans that are created in the application + * can be viewed. + * Note that the webserver is destroyed after the application ends execution. + */ zPages(); auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer(""); - std::vector> running_span_container; - while (1) { - std::string span_name; - std::cout << "Enter span name or CTRL C to exit: "; - std::cin >> span_name; - - char span_type; - std::cout << "Enter span type Error(E), Completed(C), Running(R)"; - std::cin >> span_type; - - if(span_type == 'R'){ - running_span_container.push_back(tracer->StartSpan(span_name)); - } - else if(span_type == 'C'){ - unsigned long long int start_time; - std::cout << "Start time in nanoseconds: "; - std::cin >> start_time; - - unsigned long long int end_time; - std::cout << "End time in nanoseconds: "; - std::cin >> end_time; - - opentelemetry::trace::StartSpanOptions start; - start.start_steady_time = SteadyTimestamp(nanoseconds(start_time)); - opentelemetry::trace::EndSpanOptions end; - end.end_steady_time = SteadyTimestamp(nanoseconds(end_time)); - tracer->StartSpan(span_name,start)->End(end); - } else { - std::string description; - int error_code = 0; - std::cout << "Enter an error code (integer between 1-16): "; - std::cin >> error_code; - if(error_code < 1 || error_code > 16) error_code = 0; - std::cout << "Enter a span description: "; - std::cin.get(); - getline(std::cin,description); - tracer->StartSpan(span_name)->SetStatus((opentelemetry::trace::CanonicalCode)error_code, - description); - } - std::cout << "\n"; + + std::cout << "This example for zPages creates a few types of spans and then " + << "creates a span every second for the duration of the application" + << "\n"; + + // Create a map of attributes for the span + int listInt[] = {1, 2, 3}; + std::map attribute_map; + attribute_map["attribute1"] = opentelemetry::nostd::span(listInt); + attribute_map["attribute2"] = 314159; + + // Create a span of each type(running, completed and error) + tracer->StartSpan("examplespan1",attribute_map)->End(); + + tracer->StartSpan("examplespan1")->End(); + tracer->StartSpan("examplespan1")->SetStatus( + opentelemetry::trace::CanonicalCode::CANCELLED, "Cancelled example"); + + // Create another running span with a different name + auto running_span = tracer->StartSpan("examplespan2"); + + // Create a completed span every second till user stops the loop + std::cout << "Presss CTRL+C to stop...\n"; + while(true){ + std::this_thread::sleep_for(seconds(1)); + tracer->StartSpan("examplespan")->End(); } - std::cout << "Presss to stop...\n"; - std::cin.get(); } diff --git a/examples/zpages/zpagesexample.cc b/examples/zpages/zpagesexample.cc new file mode 100644 index 0000000000..1fe0dc197c --- /dev/null +++ b/examples/zpages/zpagesexample.cc @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include + +#include "opentelemetry/ext/zpages/zpages.h" + + std::string GetRandomString() { + std::string s = ""; + const char alphanum[] = + "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + for (int i = 0; i < rand() % 8 + 2; ++i) { + s += alphanum[rand() % (sizeof(alphanum) - 1)]; + } + return s; + } + +void MakeUniqueSpans() { + auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer(""); + auto span = tracer->StartSpan(GetRandomString()); + if (rand() % 5 == 0) std::this_thread::sleep_for(std::chrono::seconds(rand() % 120)); + else std::this_thread::sleep_for(std::chrono::nanoseconds(rand() % 10000000)); + span->End(); +} + +void MakeSpans(int i) { + auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer(""); + auto span = tracer->StartSpan("span" + std::to_string(i)); + if (rand() % 5 == 0) std::this_thread::sleep_for(std::chrono::seconds(rand() % 120)); + else std::this_thread::sleep_for(std::chrono::nanoseconds(rand() % 10000000)); + span->End(); +} + +int main(int argc, char* argv[]) { + zPages(); + auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer(""); + auto run_span = tracer->StartSpan("always running"); + + while (1) { + for (int i = 0; i < 1000; i++) { + std::thread(MakeSpans, i % 5).detach(); + //std::thread(MakeUniqueSpans).detach(); + } + std::cout << "Press for more spans"; + std::cin.get(); + } + run_span->End(); +} diff --git a/ext/include/opentelemetry/ext/http/server/file_http_server.h b/ext/include/opentelemetry/ext/http/server/file_http_server.h new file mode 100644 index 0000000000..0b4d3cccc9 --- /dev/null +++ b/ext/include/opentelemetry/ext/http/server/file_http_server.h @@ -0,0 +1,145 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "opentelemetry/ext/http/server/http_server.h" + +namespace HTTP_SERVER_NS +{ + +class FileHttpServer : public HTTP_SERVER_NS::HttpServer +{ +protected: + /** + * Construct the server by initializing the endpoint for serving static files, + * which show up on the web if the user is on the given host:port. Static + * files can be seen relative to the folder where the executable was ran. + */ + FileHttpServer(const std::string &host = "127.0.0.1", int port = 3333) : HttpServer() + { + std::ostringstream os; + os << host << ":" << port; + setServerName(os.str()); + addListeningPort(port); + }; + + /** + * Set the HTTP server to serve static files from the root of host:port. + * Derived HTTP servers should initialize the file endpoint AFTER they + * initialize their own, otherwise everything will be served like a file + * @param server should be an instance of this object + */ + void InitializeFileEndpoint(FileHttpServer &server) { server[root_endpt_] = ServeFile; } + +private: + /** + * Return whether a file is found whose location is searched for relative to + * where the executable was triggered. If the file is valid, fill result with + * the file data/information required to display it on a webpage + * @param name of the file to look for, + * @param resulting file information, necessary for displaying them on a + * webpage + * @returns whether a file was found and result filled with display + * information + */ + bool FileGetSuccess(const std::string &filename, std::vector &result) + { +#ifdef _WIN32 + std::replace(filename.begin(), filename.end(), '/', '\\'); +#endif + std::streampos size; + std::ifstream file(filename, std::ios::in | std::ios::binary | std::ios::ate); + if (file.is_open()) + { + size = file.tellg(); + if (size) + { + result.resize(size); + file.seekg(0, std::ios::beg); + file.read(result.data(), size); + } + file.close(); + return true; + } + return false; + }; + + /** + * Returns the extension of a file + * @param name of the file + * @returns file extension type under HTTP protocol + */ + std::string GetMimeContentType(const std::string &filename) + { + std::string file_ext = filename.substr(filename.find_last_of(".") + 1); + auto file_type = mime_types_.find(file_ext); + return (file_type != mime_types_.end()) ? file_type->second : HTTP_SERVER_NS::CONTENT_TYPE_TEXT; + }; + + /** + * Returns the standardized name of a file by removing backslashes, and + * assuming index.html is the wanted file if a directory is given + * @param name of the file + */ + std::string GetFileName(std::string S) + { + if (S.back() == '/') + { + auto temp = S.substr(0, S.size() - 1); + S = temp; + } + // If filename appears to be a directory, serve the hypothetical index.html + // file there + if (S.find(".") == std::string::npos) + S += "/index.html"; + + return S; + } + + /** + * Sets the response object with the correct file data based on the requested + * file address, or return 404 error if a file isn't found + * @param req is the HTTP request, which we use to figure out the response to + * send + * @param resp is the HTTP response we want to send to the frontend, including + * file data + */ + HTTP_SERVER_NS::HttpRequestCallback ServeFile{ + [&](HTTP_SERVER_NS::HttpRequest const &req, HTTP_SERVER_NS::HttpResponse &resp) { + LOG_INFO("File: %s\n", req.uri.c_str()); + auto f = GetFileName(req.uri); + auto filename = f.c_str() + 1; + + std::vector content; + if (FileGetSuccess(filename, content)) + { + resp.headers[HTTP_SERVER_NS::CONTENT_TYPE] = GetMimeContentType(filename); + resp.body = std::string(content.data(), content.size()); + resp.code = 200; + resp.message = HTTP_SERVER_NS::HttpServer::getDefaultResponseMessage(resp.code); + return resp.code; + } + // Two additional 'special' return codes possible here: + // 0 - proceed to next handler + // -1 - immediately terminate and close connection + resp.headers[HTTP_SERVER_NS::CONTENT_TYPE] = HTTP_SERVER_NS::CONTENT_TYPE_TEXT; + resp.code = 404; + resp.message = HTTP_SERVER_NS::HttpServer::getDefaultResponseMessage(resp.code); + resp.body = resp.message; + return 404; + }}; + + // Maps file extensions to their HTTP-compatible mime file type + const std::unordered_map mime_types_ = { + {"css", "text/css"}, {"png", "image/png"}, {"js", "text/javascript"}, + {"htm", "text/html"}, {"html", "text/html"}, {"json", "application/json"}, + {"txt", "text/plain"}, {"jpg", "image/jpeg"}, {"jpeg", "image/jpeg"}, + }; + const std::string root_endpt_ = "/"; +}; + +} // namespace HTTP_SERVER_NS diff --git a/ext/include/opentelemetry/ext/http/server/HttpServer.h b/ext/include/opentelemetry/ext/http/server/http_server.h similarity index 97% rename from ext/include/opentelemetry/ext/http/server/HttpServer.h rename to ext/include/opentelemetry/ext/http/server/http_server.h index de6e93e5eb..672bf95c6a 100644 --- a/ext/include/opentelemetry/ext/http/server/HttpServer.h +++ b/ext/include/opentelemetry/ext/http/server/http_server.h @@ -18,7 +18,7 @@ #include #include -#include "SocketTools.h" +#include "socket_tools.h" #ifdef HAVE_HTTP_DEBUG # ifdef LOG_TRACE @@ -66,7 +66,11 @@ class HttpRequestCallback public: HttpRequestCallback(){}; - HttpRequestCallback &operator=(HttpRequestCallback other) { callback = other.callback; }; + HttpRequestCallback &operator=(HttpRequestCallback other) + { + callback = other.callback; + return *this; + }; HttpRequestCallback(CallbackFunction func) : callback(func){}; @@ -95,7 +99,6 @@ class HttpRequestCallback // - Full support of RFC 7230-7237 class HttpServer : private SocketTools::Reactor::SocketCallback { - protected: struct Connection { @@ -126,7 +129,6 @@ class HttpServer : private SocketTools::Reactor::SocketCallback class HttpRequestHandler : public std::pair { - public: HttpRequestHandler(std::string key, HttpRequestCallback *value) { @@ -142,7 +144,7 @@ class HttpServer : private SocketTools::Reactor::SocketCallback HttpRequestHandler &operator=(std::pair other) { - first = other.first; + first = other.first; second = other.second; return (*this); }; @@ -233,22 +235,16 @@ class HttpServer : private SocketTools::Reactor::SocketCallback return m_handlers.back(); } - HttpServer &operator+=( std::pair other) + HttpServer &operator+=(std::pair other) { LOG_INFO("HttpServer: Added handler for %s", other.first.c_str()); m_handlers.push_back(HttpRequestHandler(other.first, &other.second)); return (*this); }; - void start() - { - m_reactor.start(); - } + void start() { m_reactor.start(); } - void stop() - { - m_reactor.stop(); - } + void stop() { m_reactor.stop(); } protected: virtual void onSocketAcceptable(SocketTools::Socket socket) override @@ -736,8 +732,9 @@ class HttpServer : private SocketTools::Reactor::SocketCallback { LOG_TRACE("HttpServer: [%s] using handler for %s", conn.request.client.c_str(), handler.first.c_str()); - auto callback = handler.second; - int result = handler.second->onHttpRequest(conn.request, conn.response); + // auto callback = handler.second; // Bazel gets mad at this unused + // var, uncomment when using + int result = handler.second->onHttpRequest(conn.request, conn.response); if (result != 0) { conn.response.code = result; @@ -778,7 +775,6 @@ class HttpServer : private SocketTools::Reactor::SocketCallback } public: - static char const *getDefaultResponseMessage(int code) { switch (code) @@ -892,4 +888,3 @@ class HttpServer : private SocketTools::Reactor::SocketCallback }; } // namespace HTTP_SERVER_NS - diff --git a/ext/include/opentelemetry/ext/http/server/SocketTools.h b/ext/include/opentelemetry/ext/http/server/socket_tools.h similarity index 98% rename from ext/include/opentelemetry/ext/http/server/SocketTools.h rename to ext/include/opentelemetry/ext/http/server/socket_tools.h index 09c7a985a3..5c135b7a2a 100644 --- a/ext/include/opentelemetry/ext/http/server/SocketTools.h +++ b/ext/include/opentelemetry/ext/http/server/socket_tools.h @@ -94,7 +94,6 @@ namespace common /// struct Thread { - std::thread m_thread; volatile bool m_terminate{false}; @@ -153,7 +152,6 @@ namespace SocketTools // whose constructor/destructor will be called before and after main(). struct WsaInitializer { - WsaInitializer() { WSADATA wsaData; @@ -270,7 +268,6 @@ struct SocketAddr /// struct Socket { - #ifdef _WIN32 typedef SOCKET Type; static Type const Invalid = INVALID_SOCKET; @@ -454,7 +451,6 @@ struct SocketData /// struct Reactor : protected common::Thread { - /// /// Socket State callback /// @@ -732,8 +728,10 @@ struct Reactor : protected common::Thread WSANETWORKEVENTS ne; ::WSAEnumNetworkEvents(socket, m_events[index], &ne); - LOG_TRACE("Reactor: Handling socket 0x%x (index %d) with active flags 0x%x (armed 0x%x)", - static_cast(socket), index, ne.lNetworkEvents, flags); + LOG_TRACE( + "Reactor: Handling socket 0x%x (index %d) with active flags 0x%x " + "(armed 0x%x)", + static_cast(socket), index, ne.lNetworkEvents, flags); if ((flags & Readable) && (ne.lNetworkEvents & FD_READ)) { @@ -858,4 +856,4 @@ struct Reactor : protected common::Thread } }; -} // namespace SocketTools \ No newline at end of file +} // namespace SocketTools diff --git a/ext/include/opentelemetry/ext/zpages/static/tracez_index.h b/ext/include/opentelemetry/ext/zpages/static/tracez_index.h new file mode 100644 index 0000000000..7c9f5f84ed --- /dev/null +++ b/ext/include/opentelemetry/ext/zpages/static/tracez_index.h @@ -0,0 +1,44 @@ +#pragma once + +const char tracez_index[] = "" +"" +" " +" zPages TraceZ" +" " +" " +" " +" " +//" " +"

zPages TraceZ

" +" Last Updated:
" +"
" +"

" +"
" +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +"
Span NameError SamplesRunningLatency Samples
" +" " +"
" +"
Row count: 0
" +"
" +"
" +"
" +" " +"
" +" " +"
" +"
Row count: 0
" +"
" +" " +""; diff --git a/ext/include/opentelemetry/ext/zpages/static/tracez_script.h b/ext/include/opentelemetry/ext/zpages/static/tracez_script.h new file mode 100644 index 0000000000..db456164ed --- /dev/null +++ b/ext/include/opentelemetry/ext/zpages/static/tracez_script.h @@ -0,0 +1,259 @@ +#pragma once + +const char tracez_script[] = "window.onload = () => refreshData();" +"" +"const latencies = [" +" '>0s', '>10µs', '>100µs'," +" '>1ms', '>10ms', '>100ms'," +" '>1s', '>10s', '>100s'," +"];" +"" +"const statusCodeDescriptions = {" +" 'OK': 'The operation completed successfully.'," +" 'CANCELLED': 'The operation was cancelled (typically by the caller).'," +" 'UNKNOWN': `Unknown error. An example of where this error may be returned is if a Status value received" +" from another address space belongs to an error-space that is not known in this address space." +" Also errors raised by APIs that do not return enough error information may be converted to" +" this error.`," +" 'INVALID_ARGUMENT': `Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION." +" INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the" +" system (e.g., a malformed file name).`," +" 'DEADLINE_EXCEEDED': `Deadline expired before operation could complete. For operations that change the state of the" +" system, this error may be returned even if the operation has completed successfully. For" +" example, a successful response from a server could have been delayed long enough for the" +" deadline to expire.`," +" 'NOT_FOUND' : 'Some requested entity (e.g., file or directory) was not found.'," +" 'ALREADY_EXISTS': 'Some entity that we attempted to create (e.g., file or directory) already exists.'," +" 'PERMISSION_DENIED': `The caller does not have permission to execute the specified operation. PERMISSION_DENIED" +" must not be used for rejections caused by exhausting some resource (use RESOURCE_EXHAUSTED" +" instead for those errors). PERMISSION_DENIED must not be used if the caller cannot be" +" identified (use UNAUTHENTICATED instead for those errors).`," +" 'RESOURCE_EXHAUSTED': `Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system" +" is out of space.`," +" 'FAILED_PRECONDITION': `Operation was rejected because the system is not in a state required for the operation's" +" execution. For example, directory to be deleted may be non-empty, an rmdir operation is" +" applied to a non-directory, etc.`," +" 'ABORTED': `The operation was aborted, typically due to a concurrency issue like sequencer check" +" failures, transaction aborts, etc`," +" 'OUT_OF_RANGE': `Operation was attempted past the valid range. E.g., seeking or reading past end of file.`," +" 'UNIMPLEMENTED': 'Operation is not implemented or not supported/enabled in this service.'," +" 'INTERNAL': `Internal errors. Means some invariants expected by underlying system has been broken. If you" +" see one of these errors, something is very broken.`," +" 'UNAVAILABLE': `The service is currently unavailable. This is a most likely a transient condition and may be" +" corrected by retrying with a backoff.`," +" 'DATA_LOSS': 'Unrecoverable data loss or corruption.'," +" 'UNAUTHENTICATED': 'The request does not have valid authentication credentials for the operation.'," +"};" +"" +"const details = {'status': statusCodeDescriptions};" +"" +"/* Latency info is returned as an array, so they need to be parsed accordingly */" +"const getLatencyCell = (span, i, h) => `${span[h][i]}`;" +"" +"/* Pretty print a cell with a map */" +"const getKeyValueCell = (span, h) => `" +" ${JSON.stringify(span[h], null, 2)}" +" `;" +"" +"/* Standard categories when checking span details */" +"const idCols = ['spanid', 'parentid', 'traceid'];" +"const detailCols = ['attributes']; /* Columns error, running, and latency spans all share */" +"const dateCols = ['start']; /* Categories to change to date */" +"const numCols = ['duration']; /* Categories to change to num */" +"const clickCols = ['error', 'running']; /* Non-latency clickable cols */" +"const arrayCols = { " +" 'latency': getLatencyCell," +" 'events': getKeyValueCell," +" 'attributes': getKeyValueCell" +"};" +"" +"const base_endpt = '/tracez/get/'; /* For making GET requests */" +"" +"/* Maps table types to their approporiate formatting */" +"const tableFormatting = {" +" 'all': {" +" 'url': base_endpt + 'aggregations'," +" 'html_id': 'overview_table'," +" 'sizing': [" +" {'sz': 'md', 'repeats': 1}," +" {'sz': 'sm', 'repeats': 11}," +" ]," +" 'headings': ['name', ...clickCols, 'latency']," +" 'cell_headings': ['name', ...clickCols, ...latencies]," +" }," +" 'error': {" +" 'url': base_endpt + 'error/'," +" 'html_id': 'name_type_detail_table'," +" 'sizing': [" +" {'sz': 'sm', 'repeats': 6}," +" ]," +" 'headings': [...idCols, ...dateCols, 'status', ...detailCols]," +" 'has_subheading': true," +" }," +" 'running': {" +" 'url': base_endpt + 'running/'," +" 'html_id': 'name_type_detail_table'," +" 'sizing': [" +" {'sz': 'sm', 'repeats': 5}," +" ]," +" 'headings': [...idCols, ...dateCols, ...detailCols]," +" 'has_subheading': true," +" 'status': 'pending'," +" }," +" 'latency': {" +" 'url': base_endpt + 'latency/'," +" 'html_id': 'name_type_detail_table'," +" 'sizing': [" +" {'sz': 'sm', 'repeats': 6}," +" ]," +" 'headings': [...idCols, ...dateCols, ...numCols, ...detailCols]," +" 'has_subheading': true," +" 'status': 'ok'" +" }" +"};" +"const getFormat = group => tableFormatting[group];" +"" +"/* Getters using formatting config variable */" +"const getURL = group => getFormat(group)['url'];" +"const getHeadings = group => getFormat(group)['headings'];" +"const getCellHeadings = group => 'cell_headings' in getFormat(group)" +" ? getFormat(group)['cell_headings'] : getHeadings(group); " +"const getSizing = group => getFormat(group)['sizing'];" +"const getStatus = group => isLatency(group) ? 'ok' : getFormat(group)['status'];" +"const getHTML = group => getFormat(group)['html_id'];" +"" +"const isDate = col => new Set(dateCols).has(col);" +"const isLatency = group => !(new Set(clickCols).has(group)); /* non latency clickable cols, change to include latency? */" +"const isArrayCol = group => (new Set(Object.keys(arrayCols)).has(group));" +"const hasCallback = col => new Set(clickCols).has(col); /* Non-latency cb columns */" +"const hideHeader = h => new Set([...clickCols, 'name']).has(h); /* Headers to not show render twice */" +"const hasSubheading = group => isLatency(group) || 'has_subheading' in getFormat(group); " +"const hasStatus = group => isLatency(group) || 'status' in getFormat(group);" +"" +"const toTitlecase = word => word.charAt(0).toUpperCase() + word.slice(1);" +"const updateLastRefreshStr = () => document.getElementById('lastUpdateTime').innerHTML = new Date().toLocaleString();" +"" +"const getStatusHTML = group => !hasStatus(group) ? ''" +" : `All of these spans have status code ${getStatus(group)}`;" +"" +"/* Returns an HTML string that handlles width formatting" +" for a table group */" +"const tableSizing = group => ''" +" + getSizing(group).map(sz =>" +" (``).repeat(sz['repeats']))" +" .join('')" +" + '';" +"" +"/* Returns an HTML string for a table group's headings," +" hiding headings where needed */" +"const tableHeadings = group => ''" +" + getCellHeadings(group).map(h => `${(hideHeader(h) ? '' : h)}`).join('')" +" + '';" +"" +"/* Returns an HTML string, which represents the formatting for" +" the entire header for a table group. This doesn't change, and" +" includes the width formatting and the actual table headers */" +"const tableHeader = group => tableSizing(group) + tableHeadings(group);" +"" +"/* Return formatting for an array-based value based on its header */" +"const getArrayCells = (h, span) => span[h].length" +" ? (span[h].map((_, i) => arrayCols[h](span, i, h))).join('')" +" : (Object.keys(span[h]).length ? arrayCols[h](span, h) : `${emptyContent()}`);" +"" +"const emptyContent = () => `(not set)`;" +"" +"const dateStr = nanosec => {" +" const mainDate = new Date(nanosec / 1000000).toLocaleString();" +" let lostPrecision = String(nanosec % 1000000);" +" while (lostPrecision.length < 6) lostPrecision = 0 + lostPrecision;" +" const endingLocation = mainDate.indexOf('M') - 2;" +" return `${mainDate.substr(0, endingLocation)}:${lostPrecision}${mainDate.substr(endingLocation)}`;" +"};" +"" +"const detailCell = (h, span) => {" +" const detailKey = Object.keys(details[h])[span[h]];" +" const detailVal = details[h][detailKey];" +" return `" +" ${detailKey}" +" ${detailVal}" +" `;" +"};" +"" +"/* Convert cells to Date strings if needed */" +"const getCellContent = (h, span) => {" +" if (h in details) return detailCell(h, span);" +" else if (span[h] === '') return emptyContent();" +" else if (!isDate(h)) return span[h];" +" return dateStr(span[h]);" +"};" +"" +"/* Create cell based on what header we want to render */" +"const getCell = (h, span) => (isArrayCol(h)) ? getArrayCells(h, span)" +" : `` + `${getCellContent(h, span)}`;" +"" +"/* Returns an HTML string with for a span's aggregated data" +" while columns are ordered according to its table group */" +"const tableRow = (group, span) => ''" +" + getHeadings(group).map(h => getCell(h, span)).join('')" +" + '';" +"" +"/* Returns an HTML string from all the data given as" +" table rows, with each row being a group of spans by name */" +"const tableRows = (group, data) => data.map(span => tableRow(group, span)).join('');" +"" +"/* Overwrite a table on the DOM based on the group given by adding" +" its headers and fetching data for its url */" +"function overwriteTable(group, url_end = '') {" +" console.log(getURL(group) + url_end);" +" fetch(getURL(group) + url_end).then(res => res.json())" +" .then(data => {" +" console.log(data);" +" document.getElementById(getHTML(group))" +" .innerHTML = tableHeader(group)" +" + tableRows(group, data);" +" document.getElementById(getHTML(group) + '_count')" +" .innerHTML = data.length;" +" })" +" .catch(err => console.log(err));" +"};" +"" +"/* Adds a title subheading where needed */" +"function updateSubheading(group, name) {" +" if (hasSubheading(group)) {" +" document.getElementById(getHTML(isLatency(group) ? 'latency' : group) + '_header')" +" .innerHTML = `

${name}" +" ${(isLatency(group) ? `${latencies[group]} Bucket` : toTitlecase(group))}" +" Spans

Showing sampled span details (up to 5)." +" ${getStatusHTML(group)}

`;" +" }" +"};" +"" +"/* Overwrites a table on the DOM based on the group and also" +" changes the subheader, since this a looking at sampled spans */" +"function overwriteDetailedTable(group, name) {" +" if (isLatency(group)) overwriteTable('latency', group + '/' + name);" +" else overwriteTable(group, name);" +" updateSubheading(group, name);" +"};" +"" +"/* Append to a table on the DOM based on the group given */" +"function addToTable(group, url_end = '') {" +" fetch(getURL(group) + url_end).then(res => res.json())" +" .then(data => {" +" const rowsStr = tableRows(group, data);" +" if (!rowsStr) console.log(`No rows added for ${group} table`);" +" document.getElementById(getHTML(group))" +" .getElementsByTagName('tbody')[0]" +" .innerHTML += rowsStr;" +" })" +" .catch(err => console.log(err));" +"};" +"" +"const refreshData = () => {" +" updateLastRefreshStr();" +" overwriteTable('all');" +"};" +""; diff --git a/ext/include/opentelemetry/ext/zpages/static/tracez_style.h b/ext/include/opentelemetry/ext/zpages/static/tracez_style.h new file mode 100644 index 0000000000..399f53d492 --- /dev/null +++ b/ext/include/opentelemetry/ext/zpages/static/tracez_style.h @@ -0,0 +1,153 @@ +#pragma once + +const char tracez_style[] = "body {" +" color: #252525;" +" text-align: center;" +" font-family: monospace, sans-serif;" +" word-break: break-all;" +" font-size: .9em" +"}" +"" +"h1 {" +" margin: 0;" +"}" +"" +"table {" +" font-family: monospace, sans-serif;" +" border-collapse: collapse;" +" font-size: 1.05em;" +" width: 100%;" +"}" +"" +".table-wrap {" +" width: 100%;" +" min-width: 700px;" +" max-width: 2000px;" +" margin: auto;" +"}" +"" +"td, th {" +" word-break: break-word;" +" border: 1px solid #f5f5f5;" +" padding: 6px;" +" text-align: center;" +"}" +"" +"#overview_table th, #overview_table tr {" +" border-top: none;" +"}" +"" +"#headers th, #headers tr {" +" border-bottom: none;" +"}" +"" +"#top-right {" +" text-align: right;" +" position: absolute;" +" top: 10px;" +" right: 10px;" +" text-shadow: .5px .5px .25px #fff;" +"}" +"" +"#top-right button {" +" color: #f6a81c;" +" border: 2px solid #f6a81c;" +" padding: 10px;" +" margin: 10px;" +" text-transform: uppercase;" +" letter-spacing: 1px;" +" background-color: white;" +" border-radius: 10px;" +" font-weight: bold;" +"}" +"" +".right {" +" text-align: right;" +" padding: 10px;" +"}" +"" +":hover {" +" transition-duration: .15s;" +"}" +"" +"#top-right button:hover {" +" border-color: #4b5fab;" +" color: #4b5fab;" +" cursor: pointer;" +"}" +"" +"tr:nth-child(even) {" +" background-color: #eee;" +"}" +"" +".click {" +" text-decoration: underline dotted #4b5fab;" +"}" +"" +"tr:hover, td:hover, .click:hover {" +" color: white;" +" background-color: #4b5fab;" +"}" +"" +"tr:hover {" +" background-color: #4b5fabcb;" +"}" +"" +"th {" +" background-color: white;" +" color: #252525;" +"}" +"" +".click:hover {" +" cursor: pointer;" +" color: #f6a81ccc;" +"}" +"" +".empty {" +" color: #999;" +"}" +"" +".sm {" +" width: 7%;" +"}" +"" +".md {" +" width: 23%;" +"}" +"" +".lg {" +" width: 63%;" +"}" +"" +"img {" +" width: 50%;" +" max-width: 500px;" +"}" +"" +".subhead-name {" +" color: #4b5fab;" +" text-decoration: underline;" +"}" +"" +".has-tooltip {" +" text-decoration: underline dotted #f6a81c;" +"}" +"" +".has-tooltip:hover .tooltip {" +" display: block;" +"}" +"" +".tooltip {" +" display: none;" +" position: absolute;" +"}" +"" +".tooltip, .tooltip:hover {" +" background: #ffffffd9;" +" padding: 10px;" +" z-index: 1000;" +" color: #252525 !important;" +" border-radius: 10px;" +" margin: 3px 20px 0 0;" +"}" +""; diff --git a/ext/include/opentelemetry/ext/zpages/tracez_handler.h b/ext/include/opentelemetry/ext/zpages/tracez_handler.h deleted file mode 100644 index a0cededd33..0000000000 --- a/ext/include/opentelemetry/ext/zpages/tracez_handler.h +++ /dev/null @@ -1,208 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "nlohmann/json.hpp" -#include "opentelemetry/ext/zpages/zpages_http_server.h" -#include "opentelemetry/ext/zpages/tracez_data_aggregator.h" - -using json = nlohmann::json; -using namespace opentelemetry::sdk::trace; -using namespace opentelemetry::ext::zpages; - -namespace ext -{ -namespace zpages -{ - -/** Convinient data structure to convert status code enum to string **/ -const std::arraystatusCodesDescriptions = { - "OK", - "CANCELLED", - "UNKNOWN", - "INVALID_ARGUMENT", - "DEADLINE_EXCEEDED", - "NOT_FOUND", - "ALREADY_EXISTS", - "PERMISSION_DENIED", - "RESOURCE_EXHAUSTED", - "FAILED_PRECONDITION", - "ABORTED", - "OUT_OF_RANGE", - "UNIMPLEMENTED", - "INTERNAL", - "UNAVAILABLE", - "DATA_LOSS", - "UNAUTHENTICATED" -}; - -class TracezHandler { - public: - json LatencySampleSpansJSON(const std::string& name, int latency_bucket){ - auto temp = json::array(); - - if(aggregated_data_.find(name) != aggregated_data_.end()){ - const auto &latency_samples = aggregated_data_[name].sample_latency_spans[latency_bucket]; - for(const auto &latency_sample : latency_samples){ - temp.push_back({ - {"spanid", latency_sample.span_id}, - {"parentid", latency_sample.parent_id}, - {"traceid", latency_sample.trace_id}, - {"description", latency_sample.description}, - {"start", latency_sample.start_time}, - {"duration", latency_sample.duration}, - }); - } - } - - return temp; - } - - void updateAggregations() { - aggregated_data_ = data_aggregator_->GetAggregatedTracezData(); - } - - json GetErrorSampleSpanJSON(const std::string& name){ - auto temp = json::array(); - - if(aggregated_data_.find(name) != aggregated_data_.end()){ - const auto &error_samples = aggregated_data_[name].sample_error_spans; - for(const auto &error_sample : error_samples){ - temp.push_back({ - {"spanid", error_sample.span_id}, - {"parentid", error_sample.parent_id}, - {"traceid", error_sample.trace_id}, - {"description", error_sample.description}, - {"start", error_sample.start_time}, - {"status",statusCodesDescriptions[error_sample.status_code]}, - }); - } - } - return temp; - } - - json GetRunningSampleSpanJSON(const std::string& name) { - auto temp = json::array(); - if(aggregated_data_.find(name) != aggregated_data_.end()){ - const auto &running_samples = aggregated_data_[name].sample_running_spans; - for(const auto &running_sample : running_samples){ - temp.push_back({ - {"spanid", running_sample.span_id}, - {"parentid", running_sample.parent_id}, - {"traceid", running_sample.trace_id}, - {"description", running_sample.description}, - {"start", running_sample.start_time}, - }); - } - } - return temp; - } - - json GetAggregationSummary() { - updateAggregations(); - auto temp = json::array(); - for(const auto &aggregation_group: aggregated_data_){ - const auto &buckets = aggregation_group.second; - const auto &complete_ok_counts = buckets.completed_span_count_per_latency_bucket; - auto latency = json::array(); - for (int i = 0; i < 9; i++) { - latency.push_back(complete_ok_counts[i]); - } - temp.push_back({ - {"name", aggregation_group.first}, - {"error", buckets.error_span_count}, - {"running", buckets.running_span_count}, - {"latency", latency} - }); - } - return temp; - } - - TracezHandler(std::unique_ptr &&aggregator) { - data_aggregator_ = std::move(aggregator); - }; - - // Return query after endpoint - std::string GetSuffix(const std::string& uri) { - if (endpoint_.length() + 1 > uri.length()) return uri; - return uri.substr(endpoint_.length() + 1); - } - - // Returns whether str starts with pre - bool StartsWith(const std::string& str, std::string pre) { - if (pre.length() > str.length()) return false; - return str.substr(0, pre.length()) == pre; - } - - // Check if str starts with endpoint - bool StartsWith(const std::string& str) { - return StartsWith(str, endpoint_); - } - - // Returns string after leftmost backslash. Used for getting bucket number - // primarily in latency - std::string GetAfterSlash(const std::string& str) { - const auto& backslash = str.find("/"); - if (backslash == std::string::npos || backslash == str.length()) return ""; - return str.substr(backslash + 1); - } - - // Returns string before leftmost backslash. Used for getting aggregation group - // name - std::string GetBeforeSlash(const std::string& str) { - const auto& backslash = str.find("/"); - if (backslash == std::string::npos || backslash == str.length()) return str; - return str.substr(0, backslash); - } - - HTTP_SERVER_NS::HttpRequestCallback Serve{[&](HTTP_SERVER_NS::HttpRequest const& req, - HTTP_SERVER_NS::HttpResponse& resp) { - resp.headers[testing::CONTENT_TYPE] = "application/json"; - if (StartsWith(req.uri)) { - std::string query = GetSuffix(req.uri); - if (StartsWith(query, "latency")) { - auto queried_bucket_name = GetAfterSlash(query); - auto queried_latency_index = std::stoi(GetBeforeSlash(queried_bucket_name)); - auto queried_name = GetAfterSlash(queried_bucket_name); - resp.body = LatencySampleSpansJSON(queried_name, queried_latency_index).dump(); - } - else { - auto queried_name = GetAfterSlash(query); - - if (StartsWith(query, "aggregations")) { - resp.body = GetAggregationSummary().dump(); - } - else if (StartsWith(query, "running")) { - resp.body = GetRunningSampleSpanJSON(queried_name).dump(); - } - else if (StartsWith(query, "error")) { - resp.body = GetErrorSampleSpanJSON(queried_name).dump(); - } - } - } - else { - resp.body = GetAggregationSummary().dump(); - }; - return 200; - }}; - - std::string GetEndpoint() { - return endpoint_; - } - - private: - const std::string endpoint_ = "/tracez/get"; - std::map aggregated_data_; - std::unique_ptr data_aggregator_; - -}; - -} // namespace zpages -} // namespace ext diff --git a/ext/include/opentelemetry/ext/zpages/tracez_http_server.h b/ext/include/opentelemetry/ext/zpages/tracez_http_server.h index 74f3c5b491..357b61e042 100644 --- a/ext/include/opentelemetry/ext/zpages/tracez_http_server.h +++ b/ext/include/opentelemetry/ext/zpages/tracez_http_server.h @@ -9,8 +9,14 @@ #include "opentelemetry/ext/zpages/tracez_data_aggregator.h" #include "opentelemetry/ext/zpages/zpages_http_server.h" +#include "opentelemetry/ext/zpages/static/tracez_index.h" +#include "opentelemetry/ext/zpages/static/tracez_style.h" +#include "opentelemetry/ext/zpages/static/tracez_script.h" #include "nlohmann/json.hpp" +#define HAVE_HTTP_DEBUG +#define HAVE_CONSOLE_LOG + using json = nlohmann::json; OPENTELEMETRY_BEGIN_NAMESPACE @@ -19,7 +25,7 @@ namespace zpages { class TracezHttpServer : public opentelemetry::ext::zpages::zPagesHttpServer { public: - /* + /** * Construct the server by initializing the endpoint for querying TraceZ aggregation data and files, * along with taking ownership of the aggregator whose data is used to send data to the frontend * @param aggregator is the TraceZ Data Aggregator, which calculates aggregation info @@ -27,15 +33,14 @@ class TracezHttpServer : public opentelemetry::ext::zpages::zPagesHttpServer { * @param port is the port where the TraceZ webpages will be displayed, default being 30000 */ TracezHttpServer(std::unique_ptr &&aggregator, - std::string host = "localhost", int port = 30000) : - opentelemetry::ext::zpages::zPagesHttpServer("/tracez/get", host, port), + const std::string& host = "localhost", int port = 30000) : + opentelemetry::ext::zpages::zPagesHttpServer("/tracez", host, port), data_aggregator_(std::move(aggregator)) { InitializeTracezEndpoint(*this); - InitializeFileEndpoint(*this); }; private: - /* + /** * Set the HTTP server to use the "Serve" callback to send the appropriate data when queried * @param server, which should be an instance of this object */ @@ -43,19 +48,19 @@ class TracezHttpServer : public opentelemetry::ext::zpages::zPagesHttpServer { server[endpoint_] = Serve; } - /* + /** * Updates the stored aggregation data (aggregations_) using the data aggregator */ void UpdateAggregations(); - - /* + + /** * First updates the stored aggregations, then translates that data from a C++ map to * a JSON object * @returns JSON object of collected spans bucket counts by name */ json GetAggregations(); - /* + /** * Using the stored aggregations, finds the span group with the right name and returns * its running span data as a JSON, only grabbing the fields needed for the frontend * @param name of the span group whose running data we want @@ -63,7 +68,7 @@ class TracezHttpServer : public opentelemetry::ext::zpages::zPagesHttpServer { */ json GetRunningSpansJSON(const std::string& name); - /* + /** * Using the stored aggregations, finds the span group with the right name and returns * its error span data as a JSON, only grabbing the fields needed for the frontend * @param name of the span group whose running data we want @@ -71,45 +76,75 @@ class TracezHttpServer : public opentelemetry::ext::zpages::zPagesHttpServer { */ json GetErrorSpansJSON(const std::string& name); - /* + /** * Using the stored aggregations, finds the span group with the right name and bucket index * returning its latency span data as a JSON, only grabbing the fields needed for the frontend * @param name of the span group whose latency data we want * @param index of which latency bucket to grab from * @returns JSON representing bucket span data with the passed in name and latency range */ - json GetLatencySpansJSON(const std::string& name, const int& latency_range_index); - - /* + json GetLatencySpansJSON(const std::string& name, int latency_range_index); + + /** + * Returns attributes, which have varied types, from a span data to convert into JSON + * @param sample current span data, whose attributes we want to extract + * @returns JSON representing attributes for a given threadsafe span data + */ + json GetAttributesJSON(const opentelemetry::ext::zpages::ThreadsafeSpanData& sample); + + /** * Sets the response object with the TraceZ aggregation data based on the request endpoint * @param req is the HTTP request, which we use to figure out the response to send * @param resp is the HTTP response we want to send to the frontend, either webpage or TraceZ - * aggregation data + * aggregation data */ HTTP_SERVER_NS::HttpRequestCallback Serve{[&](HTTP_SERVER_NS::HttpRequest const& req, HTTP_SERVER_NS::HttpResponse& resp) { - resp.headers[testing::CONTENT_TYPE] = "application/json"; - - std::string query = GetQuery(req.uri); - - if (StartsWith(query, "latency")) { - auto queried_latency_name = GetAfterSlash(query); - auto queried_latency_index = std::stoi(GetBeforeSlash(queried_latency_name)); - auto queried_name = GetAfterSlash(queried_latency_name); - resp.body = GetLatencySpansJSON(queried_name, queried_latency_index).dump(); + std::string query = GetQuery(req.uri); // tracez + + if (StartsWith(query, "get")) { + resp.headers[HTTP_SERVER_NS::CONTENT_TYPE] = "application/json"; + query = GetAfterSlash(query); + if (StartsWith(query, "latency")) { + auto queried_latency_name = GetAfterSlash(query); + auto queried_latency_index = std::stoi(GetBeforeSlash(queried_latency_name)); + auto queried_name = GetAfterSlash(queried_latency_name); + resp.body = GetLatencySpansJSON(queried_name, queried_latency_index).dump(); + } + else { + auto queried_name = GetAfterSlash(query); + if (StartsWith(query, "aggregations")) { + resp.body = GetAggregations().dump(); + } + else if (StartsWith(query, "running")) { + resp.body = GetRunningSpansJSON(queried_name).dump(); + } + else if (StartsWith(query, "error")) { + resp.body = GetErrorSpansJSON(queried_name).dump(); + } + else { + resp.body = json::array(); + } + } } else { - auto queried_name = GetAfterSlash(query); - if (StartsWith(query, "aggregations")) { - resp.body = GetAggregations().dump(); + //resp.body = StartsWith(query, "index.html") ? "cool" : " no "; + if (StartsWith(query, "script.js")) { + resp.headers[HTTP_SERVER_NS::CONTENT_TYPE] = "text/javascript"; + resp.body = tracez_script; + } + else if (StartsWith(query, "style.css")) { + resp.headers[HTTP_SERVER_NS::CONTENT_TYPE] = "text/css"; + resp.body = tracez_style; } - else if (StartsWith(query, "running")) { - resp.body = GetRunningSpansJSON(queried_name).dump(); + else if (query.empty() || query == "/tracez" || StartsWith(query, "index.html")) { + resp.headers[HTTP_SERVER_NS::CONTENT_TYPE] = "text/html"; + resp.body = tracez_index; } - else if (StartsWith(query, "error")) { - resp.body = GetErrorSpansJSON(queried_name).dump(); + else { + resp.headers[HTTP_SERVER_NS::CONTENT_TYPE] = "text/plain"; + resp.body = "Invalid query: " + query; } - else resp.body = json::array(); } return 200; diff --git a/ext/include/opentelemetry/ext/zpages/zpages_http_server.h b/ext/include/opentelemetry/ext/zpages/zpages_http_server.h index ca0ec06928..e94c185291 100644 --- a/ext/include/opentelemetry/ext/zpages/zpages_http_server.h +++ b/ext/include/opentelemetry/ext/zpages/zpages_http_server.h @@ -7,7 +7,7 @@ #include #include -#include "opentelemetry/ext/http/server/HttpServer.h" +#include "opentelemetry/ext/http/server/http_server.h" OPENTELEMETRY_BEGIN_NAMESPACE namespace ext { @@ -15,7 +15,7 @@ namespace zpages { class zPagesHttpServer : public HTTP_SERVER_NS::HttpServer { protected: - /* + /** * Construct the server by initializing the endpoint for serving static files, which show up on the * web if the user is on the given host:port. Static files can be seen relative to the folder where the * executable was ran. @@ -23,163 +23,52 @@ class zPagesHttpServer : public HTTP_SERVER_NS::HttpServer { * @param port is the port where the TraceZ webpages will be displayed * @param endpoint is where this specific zPage will server files */ - zPagesHttpServer(const std::string& endpoint, std::string host, int port) : HttpServer(), endpoint_(endpoint) { + zPagesHttpServer(const std::string& endpoint, const std::string& host = "127.0.0.1", int port = 52620) : HttpServer(), endpoint_(endpoint) { std::ostringstream os; os << host << ":" << port; setServerName(os.str()); addListeningPort(port); }; - /* - * Set the HTTP server to serve static files from the root of host:port - * @param server should be an instance of this object - */ - void InitializeFileEndpoint(zPagesHttpServer& server) { - server[root_endpt_] = ServeFile; - } - - /* - * Helper function tat returns query information by isolating it from the base endpoint + /** + * Helper function that returns query information by isolating it from the base endpoint + * @param uri is the full query */ std::string GetQuery(const std::string& uri) { if (endpoint_.length() + 1 > uri.length()) return uri; return uri.substr(endpoint_.length() + 1); } - /* + /** * Helper that returns whether a str starts with pre * @param str is the string we're checking * @param pre is the prefix we're checking against */ - bool StartsWith(const std::string& str, std::string pre) { - return (pre.length() > str.length()) - ? false - : str.substr(0, pre.length()) == pre; + bool StartsWith(const std::string& str, const std::string& pre) { + return str.rfind(pre, 0) == 0; } - /* + /** * Helper that returns the remaining string after the leftmost backslash * @param str is the string we're extracting from */ std::string GetAfterSlash(const std::string& str) { - const auto& backslash = str.find("/"); + std::size_t backslash = str.find("/"); if (backslash == std::string::npos || backslash == str.length()) return ""; return str.substr(backslash + 1); } - /* + /** * Helper that returns the remaining string after the leftmost backslash * @param str is the string we're extracting from */ std::string GetBeforeSlash(const std::string& str) { - const auto& backslash = str.find("/"); + std::size_t backslash = str.find("/"); if (backslash == std::string::npos || backslash == str.length()) return str; return str.substr(0, backslash); } - - const std::string endpoint_; - - private: - /* - * Return whether a file is found whose location is searched for relative to where - * the executable was triggered. If the file is valid, fill result with the file data/information - * required to display it on a webpage - * @param name of the file to look for, - * @param resulting file information, necessary for displaying them on a webpage - * @returns whether a file was found and result filled with display information - */ - bool FileGetSuccess (std::string filename, std::vector& result) { - #ifdef _WIN32 - std::replace(filename.begin(), filename.end(), '/', '\\'); - #endif - std::streampos size; - std::ifstream file(filename, std::ios::in | std::ios::binary | std::ios::ate); - if (file.is_open()) { - size = file.tellg(); - if (size) { - result.resize(size); - file.seekg(0, std::ios::beg); - file.read(result.data(), size); - } - file.close(); - return true; - } - return false; - }; - - /* - * Returns the extension of a file - * @param name of the file - * @returns file extension type under HTTP protocol - */ - std::string GetMimeContentType(std::string filename) { - std::string file_ext = filename.substr(filename.find_last_of(".") + 1); - auto file_type = mime_types_.find(file_ext); - return (file_type != mime_types_.end()) - ? file_type->second - : HTTP_SERVER_NS::CONTENT_TYPE_TEXT; - }; - - /* - * Returns the standardized name of a file by removing backslashes, and - * assuming index.html is the wanted file if a directory is given - * @param name of the file - */ - std::string GetFileName(std::string S) { - if (S.back() == '/') { - auto temp = S.substr(0, S.size() - 1); - S = temp; - } - // If filename appears to be a directory, serve the hypothetical index.html file there - if (S.find(".") == std::string::npos) S += "/index.html"; - - return S; - } - /* - * Sets the response object with the correct file data based on the requested file address, - * or return 404 error if a file isn't found - * @param req is the HTTP request, which we use to figure out the response to send - * @param resp is the HTTP response we want to send to the frontend, including file data - */ - HTTP_SERVER_NS::HttpRequestCallback ServeFile{[&](HTTP_SERVER_NS::HttpRequest const& req, - HTTP_SERVER_NS::HttpResponse& resp) { - LOG_INFO("File: %s\n", req.uri.c_str()); - auto f = GetFileName(req.uri); - auto filename = f.c_str() + 1; - - std::vector content; - if (FileGetSuccess(filename, content)) { - resp.headers[HTTP_SERVER_NS::CONTENT_TYPE] = GetMimeContentType(filename); - resp.body = std::string(content.data(), content.size()); - resp.code = 200; - resp.message = HTTP_SERVER_NS::HttpServer::getDefaultResponseMessage(resp.code); - return resp.code; - } - // Two additional 'special' return codes possible here: - // 0 - proceed to next handler - // -1 - immediately terminate and close connection - resp.headers[HTTP_SERVER_NS::CONTENT_TYPE] = HTTP_SERVER_NS::CONTENT_TYPE_TEXT; - resp.code = 404; - resp.message = HTTP_SERVER_NS::HttpServer::getDefaultResponseMessage(resp.code); - resp.body = resp.message; - return 404; - }}; - - - // Maps file extensions to their HTTP-compatible mime file type - const std::unordered_map mime_types_ = { - {"css", "text/css"}, - {"png", "image/png"}, - {"js", "text/javascript"}, - {"htm", "text/html"}, - {"html", "text/html"}, - {"json", "application/json"}, - {"txt", "text/plain"}, - {"jpg", "image/jpeg"}, - {"jpeg", "image/jpeg"}, - }; - const std::string root_endpt_ = "/"; + const std::string endpoint_; }; diff --git a/ext/src/zpages/tracez_http_server.cc b/ext/src/zpages/tracez_http_server.cc index 1ace4b0ed1..aef35b17eb 100644 --- a/ext/src/zpages/tracez_http_server.cc +++ b/ext/src/zpages/tracez_http_server.cc @@ -4,13 +4,9 @@ OPENTELEMETRY_BEGIN_NAMESPACE namespace ext { namespace zpages { - void TracezHttpServer::UpdateAggregations() { - aggregated_data_ = data_aggregator_->GetAggregatedTracezData(); - } - json TracezHttpServer::GetAggregations() { - UpdateAggregations(); - auto counts = json::array(); + aggregated_data_ = data_aggregator_->GetAggregatedTracezData(); + auto counts_json = json::array(); for(const auto &aggregation_group: aggregated_data_){ const auto &buckets = aggregation_group.second; @@ -21,14 +17,14 @@ namespace zpages { latency_counts.push_back(complete_ok_counts[boundary]); } - counts.push_back({ + counts_json.push_back({ {"name", aggregation_group.first}, {"error", buckets.error_span_count}, {"running", buckets.running_span_count}, {"latency", latency_counts} }); } - return counts; + return counts_json; } json TracezHttpServer::GetRunningSpansJSON(const std::string& name) { @@ -47,12 +43,13 @@ namespace zpages { {"traceid", std::string( reinterpret_cast(sample.GetTraceId().Id().data()))}, {"start", sample.GetStartTime().time_since_epoch().count()}, + {"attributes", GetAttributesJSON(sample)}, }); } } return running_json; } - + json TracezHttpServer::GetErrorSpansJSON(const std::string& name) { auto error_json = json::array(); @@ -60,45 +57,92 @@ namespace zpages { if(grouping != aggregated_data_.end()){ const auto &error_samples = grouping->second.sample_error_spans; - for(const auto &error_sample : error_samples){ + for(const auto &sample : error_samples){ error_json.push_back({ {"spanid", std::string( - reinterpret_cast(error_sample.GetSpanId().Id().data()))}, + reinterpret_cast(sample.GetSpanId().Id().data()))}, {"parentid", std::string(reinterpret_cast( - error_sample.GetParentSpanId().Id().data()))}, + sample.GetParentSpanId().Id().data()))}, {"traceid", std::string( - reinterpret_cast(error_sample.GetTraceId().Id().data()))}, - {"start", error_sample.GetStartTime().time_since_epoch().count()}, - {"status", (unsigned short)error_sample.GetStatus()} + reinterpret_cast(sample.GetTraceId().Id().data()))}, + {"start", sample.GetStartTime().time_since_epoch().count()}, + {"status", (unsigned short)sample.GetStatus()}, + {"attributes", GetAttributesJSON(sample)}, }); } } return error_json; } - json TracezHttpServer::GetLatencySpansJSON(const std::string& name, const int& latency_range_index){ + json TracezHttpServer::GetLatencySpansJSON(const std::string& name, int latency_range_index){ auto latency_json = json::array(); auto grouping = aggregated_data_.find(name); if(grouping != aggregated_data_.end()){ const auto &latency_samples = grouping->second.sample_latency_spans[latency_range_index]; - for(const auto &latency_sample : latency_samples){ + for(const auto &sample : latency_samples){ latency_json.push_back({ {"spanid", std::string( - reinterpret_cast(latency_sample.GetSpanId().Id().data()))}, + reinterpret_cast(sample.GetSpanId().Id().data()))}, {"parentid", std::string(reinterpret_cast( - latency_sample.GetParentSpanId().Id().data()))}, + sample.GetParentSpanId().Id().data()))}, {"traceid", std::string( - reinterpret_cast(latency_sample.GetTraceId().Id().data()))}, - {"start", latency_sample.GetStartTime().time_since_epoch().count()}, - {"duration", latency_sample.GetDuration().count()}, + reinterpret_cast(sample.GetTraceId().Id().data()))}, + {"start", sample.GetStartTime().time_since_epoch().count()}, + {"duration", sample.GetDuration().count()}, + {"attributes", GetAttributesJSON(sample)}, }); } } return latency_json; } + json TracezHttpServer::GetAttributesJSON(const opentelemetry::ext::zpages::ThreadsafeSpanData& sample) { + auto attributes_json = json::object(); + for (const auto &sample_attribute : sample.GetAttributes()) { + auto& key = sample_attribute.first; + auto& val = sample_attribute.second; // SpanDataAttributeValue + + /* Convert variant types to into their nonvariant form. This is done this way because + the frontend and JSON doesn't care about type, and variant's get function only allows + const integers or literals */ + + switch(val.index()) { + case 0: + attributes_json[key] = opentelemetry::nostd::get<0>(val); + break; + case 1: + attributes_json[key] = opentelemetry::nostd::get<1>(val); + break; + case 2: + attributes_json[key] = opentelemetry::nostd::get<2>(val); + break; + case 3: + attributes_json[key] = opentelemetry::nostd::get<3>(val); + break; + case 4: + attributes_json[key] = opentelemetry::nostd::get<4>(val); + break; + case 5: + attributes_json[key] = opentelemetry::nostd::get<5>(val); + break; + case 6: + attributes_json[key] = opentelemetry::nostd::get<6>(val); + break; + case 7: + attributes_json[key] = opentelemetry::nostd::get<7>(val); + break; + case 8: + attributes_json[key] = opentelemetry::nostd::get<8>(val); + break; + case 9: + attributes_json[key] = opentelemetry::nostd::get<9>(val); + break; + } + } + return attributes_json; + } } // namespace zpages } // namespace ext diff --git a/ext/src/zpages/zpages_static_files/images/opentelemetry.png b/ext/src/zpages/zpages_static_files/images/opentelemetry.png deleted file mode 100644 index ac6f4bd7e2..0000000000 Binary files a/ext/src/zpages/zpages_static_files/images/opentelemetry.png and /dev/null differ diff --git a/ext/src/zpages/zpages_static_files/tracez/index.html b/ext/src/zpages/zpages_static_files/tracez/index.html deleted file mode 100644 index 6fd6ae0644..0000000000 --- a/ext/src/zpages/zpages_static_files/tracez/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - zPages TraceZ - - - - - -

zPages TraceZ

-

Last Updated:

- -

-
- - - - - - - - - - - - - -
Span NameError SamplesRunningLatency Samples
- -
-
-
-
- -
- -
-
- - diff --git a/ext/src/zpages/zpages_static_files/tracez/script.js b/ext/src/zpages/zpages_static_files/tracez/script.js deleted file mode 100644 index c63bc258e4..0000000000 --- a/ext/src/zpages/zpages_static_files/tracez/script.js +++ /dev/null @@ -1,200 +0,0 @@ -window.onload = () => refreshData(); - -const latencies = [ - '>0s', '>10µs', '>100µs', - '>1ms', '>10ms', '>100ms', - '>1s', '>10s', '>100s', -]; - -// Latency info is returned as an array, so they need to be parsed accordingly -const getLatencyCell = (span, i, h) => `${span[h][i]}`; - -// Pretty print a cell with a map -const getKeyValueCell = (span, i, h) => ` - ${JSON.stringify(span[h][i], null, 2)} - `; - -// Standard categories when checking span details -const idCols = ['spanid', 'parentid', 'traceid'] -const detailCols = ['description']; // Columns error, running, and latency spans all share -const dateCols = ['start']; // Categories to change to date -const numCols = ['duration']; // Categories to change to num -const clickCols = ['error', 'running']; // Non-latency clickable cols -const arrayCols = { - 'latency': getLatencyCell, - 'events': getKeyValueCell, - 'attributes': getKeyValueCell -}; - -const base_endpt = '/tracez/get/'; // For making GET requests -// Maps table types to their approporiate formatting -const tableFormatting = { - 'all': { - 'url': base_endpt + 'aggregations', - 'html_id': 'overview_table', - 'sizing': [ - {'sz': 'md', 'repeats': 1}, - {'sz': 'sm', 'repeats': 11}, - ], - 'headings': ['name', ...clickCols, 'latency'], - 'cell_headings': ['name', ...clickCols, ...latencies], - }, - 'error': { - 'url': base_endpt + 'error/', - 'html_id': 'name_type_detail_table', - 'sizing': [ - {'sz': 'sm', 'repeats': 5}, - {'sz': 'md', 'repeats': 1}, - ], - 'headings': [...idCols, ...dateCols, 'status', ...detailCols], - 'has_subheading': true, - }, - 'running': { - 'url': base_endpt + 'running/', - 'html_id': 'name_type_detail_table', - 'sizing': [ - {'sz': 'sm', 'repeats': 4}, - {'sz': 'md', 'repeats': 1}, - ], - 'headings': [...idCols, ...dateCols, ...detailCols], - 'has_subheading': true, - 'status': 'pending', - }, - 'latency': { - 'url': base_endpt + 'latency/', - 'html_id': 'name_type_detail_table', - 'sizing': [ - {'sz': 'sm', 'repeats': 5}, - {'sz': 'md', 'repeats': 1}, - ], - 'headings': [...idCols, ...dateCols, ...numCols, ...detailCols], - 'has_subheading': true, - 'status': 'ok' - } -}; -const getFormat = group => tableFormatting[group]; - - -// Getters using formatting config variable -const getURL = group => getFormat(group)['url']; -const getHeadings = group => getFormat(group)['headings']; -const getCellHeadings = group => 'cell_headings' in getFormat(group) - ? getFormat(group)['cell_headings'] : getHeadings(group); -const getSizing = group => getFormat(group)['sizing']; -const getStatus = group => isLatency(group) ? 'ok' : getFormat(group)['status']; -const getHTML = group => getFormat(group)['html_id']; - -const isDate = col => new Set(dateCols).has(col); -const isLatency = group => !(new Set(clickCols).has(group)); // non latency clickable cols, change to include latency? -const isArrayCol = group => (new Set(Object.keys(arrayCols)).has(group)); -const hasCallback = col => new Set(clickCols).has(col); //!isLatency(col); // Non-latency cb columns -const hideHeader = h => new Set([...clickCols, 'name']).has(h); // Headers to not show render twice -const hasSubheading = group => isLatency(group) || 'has_subheading' in getFormat(group); -const hasStatus = group => isLatency(group) || 'status' in getFormat(group); - -const toTitlecase = word => word.charAt(0).toUpperCase() + word.slice(1); -const updateLastRefreshStr = () => document.getElementById('lastUpdateTime').innerHTML = new Date().toLocaleString(); // update - -const getStatusHTML = group => !hasStatus(group) ? '' - : `All of these spans have status code ${getStatus(group)}`; - -// Returns an HTML string that handlles width formatting -// for a table group -const tableSizing = group => '' - + getSizing(group).map(sz => - (``).repeat(sz['repeats'])) - .join('') - + ''; - -// Returns an HTML string for a table group's headings, -// hiding headings where needed -const tableHeadings = group => '' - + getCellHeadings(group).map(h => `${(hideHeader(h) ? '' : h)}`).join('') - + ''; - -// Returns an HTML string, which represents the formatting for -// the entire header for a table group. This doesn't change, and -// includes the width formatting and the actual table headers -const tableHeader = group => tableSizing(group) + tableHeadings(group); - -// Return formatting for an array-based value based on its header -const getArrayCells = (h, span) => span[h].length - ? (span[h].map((_, i) => arrayCols[h](span, i, h))).join('') - : 'Empty'; - -const EmptyContent = () => `(not set)` - -// Convert cells to Date strings if needed -const getCellContent = (h, span) => { - if (!isDate(h)) return (span[h] !== '') ? span[h] : EmptyContent(); - return new Date(span[h] / 1000000).toJSON(); -}; - -// Create cell based on what header we want to render -const getCell = (h, span) => (isArrayCol(h)) ? getArrayCells(h, span) - : `` + `${getCellContent(h, span)}`; - -// Returns an HTML string with for a span's aggregated data -// while columns are ordered according to its table group -const tableRow = (group, span) => '' - + getHeadings(group).map(h => getCell(h, span)).join('') - + ''; - -// Returns an HTML string from all the data given as -// table rows, with each row being a group of spans by name -const tableRows = (group, data) => data.map(span => tableRow(group, span)).join(''); - -// Overwrite a table on the DOM based on the group given by adding -// its headers and fetching data for its url -function overwriteTable(group, url_end = '') { - console.log(getURL(group) + url_end); - fetch(getURL(group) + url_end).then(res => res.json()) - .then(data => { - console.log(data); - document.getElementById(getHTML(group)) - .innerHTML = tableHeader(group) - + tableRows(group, data); - }) - .catch(err => console.log(err)); -}; - -// Adds a title subheading where needed -function updateSubheading(group, name) { - if (hasSubheading(group)) { - document.getElementById(getHTML(isLatency(group) ? 'latency' : group) + '_header') - .innerHTML = `

${name}
- ${(isLatency(group) ? `${latencies[group]} Bucket` : toTitlecase(group))} - Spans

Showing sampled span details (up to 5). - ${getStatusHTML(group)}

`; - } -}; - -// Overwrites a table on the DOM based on the group and also -// changes the subheader, since this a looking at sampled spans -function overwriteDetailedTable(group, name) { - if (isLatency(group)) overwriteTable('latency', group + '/' + name); - else overwriteTable(group, name); - updateSubheading(group, name); -}; - -// Append to a table on the DOM based on the group given -function addToTable(group, url_end = '') { - fetch(getURL(group) + url_end).then(res => res.json()) - .then(data => { - const rowsStr = tableRows(group, data); - if (!rowsStr) console.log(`No rows added for ${group} table`); - document.getElementById(getHTML(group)) - .getElementsByTagName('tbody')[0] - .innerHTML += rowsStr; - }) - .catch(err => console.log(err)); -}; - -const refreshData = () => { - updateLastRefreshStr(); - overwriteTable('all'); -}; - diff --git a/ext/src/zpages/zpages_static_files/tracez/style.css b/ext/src/zpages/zpages_static_files/tracez/style.css deleted file mode 100644 index cbb92a9173..0000000000 --- a/ext/src/zpages/zpages_static_files/tracez/style.css +++ /dev/null @@ -1,111 +0,0 @@ -body { - color: #252525; - text-align: center; - font-family: monospace, sans-serif; - word-break: break-all; - font-size: .9em -} - -table { - font-family: monospace, sans-serif; - border-collapse: collapse; - font-size: 1.05em; - width: 100%; -} - -.table-wrap { - width: 100%; - min-width: 700px; - max-width: 2000px; - margin: auto; -} - -td, th { - word-break: break-word; - border: 1px solid #f5f5f5; - padding: 6px; - text-align: center; -} - -#overview_table th, #overview_table tr { - border-top: none; -} - -#headers th, #headers tr { - border-bottom: none; -} - -#top-right { - position: absolute; - top: 10px; - right: 10px -} - -#top-right button { - color: #f6a81c; - border: 2px solid #f6a81c; - padding: 8px; - text-transform: uppercase; - letter-spacing: 1px; - background-color: white; - border-radius: 10px; - font-weight: bold; -} - -:hover { - transition-duration: .15s; -} - -#top-right button:hover { - border-color: #4b5fab; - color: #4b5fab; - cursor: pointer; -} - -tr:nth-child(even) { - background-color: #eee; -} - -.click { - text-decoration: underline dotted #4b5fab; -} - -tr:hover, td:hover, .click:hover { - color: white; - background-color: #4b5fab; -} - -tr:hover { - background-color: #4b5fabcb; -} - -th { - background-color: white; - color: #252525; -} - -.click:hover { - cursor: pointer; - color: #f6a81ccc; -} - -.empty { - color: #999; -} - -.sm { - width: 7%; -} - -.md { - width: 23%; -} - -.lg { - width: 63%; -} - -img { - width: 50%; - max-width: 500px; -}