diff --git a/.github/workflows/nginx.yml b/.github/workflows/nginx.yml new file mode 100644 index 000000000..04d5ffc43 --- /dev/null +++ b/.github/workflows/nginx.yml @@ -0,0 +1,72 @@ +name: nginx instrumentation CI + +on: + push: + branches: "*" + pull_request: + branches: [ main ] + +jobs: + test-nginx-agent: + name: Test nginx agent + runs-on: ubuntu-20.04 + steps: + - name: checkout otel nginx + uses: actions/checkout@v2 + - name: setup + run: | + sudo ./instrumentation/nginx/ci/setup_test_environment.sh + - name: build docker images + run: | + cd instrumentation/nginx + docker build -t otel-nginx-test/nginx -f test/Dockerfile . + docker build -t otel-nginx-test/express-backend -f test/backend/simple_express/Dockerfile test/backend/simple_express + - name: run tests + run: | + cd instrumentation/nginx/test/instrumentation + mix local.hex --force --if-missing + mix local.rebar --force --if-missing + mix deps.get + mix test + build-nginx-agent: + name: Build nginx agent + runs-on: ubuntu-20.04 + timeout-minutes: 30 + steps: + - name: checkout otel nginx + uses: actions/checkout@v2 + - name: setup + run: | + sudo ./instrumentation/nginx/ci/setup_ci_environment.sh + - name: checkout grpc + uses: actions/checkout@v2 + with: + repository: "grpc/grpc" + ref: "v1.35.0" + fetch-depth: 1 + path: "instrumentation/nginx/grpc" + submodules: "recursive" + - name: build grpc + run: | + cd instrumentation/nginx + ./ci/grpc.sh + - name: checkout opentelemetry-cpp + uses: actions/checkout@v2 + with: + repository: "open-telemetry/opentelemetry-cpp" + fetch-depth: 1 + path: "instrumentation/nginx/opentelemetry-cpp" + submodules: "recursive" + - name: build opentelemetry-cpp + run: | + cd instrumentation/nginx + ./ci/otel-cpp.sh + - name: build nginx module + run: | + cd instrumentation/nginx + ./ci/build_module.sh + - name: Upload module artifact + uses: actions/upload-artifact@v2 + with: + name: otel-nginx + path: ./instrumentation/nginx/build/otel_ngx_module.so diff --git a/instrumentation/nginx/CMakeLists.txt b/instrumentation/nginx/CMakeLists.txt new file mode 100644 index 000000000..d589a49c9 --- /dev/null +++ b/instrumentation/nginx/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.12) + +project(opentelemetry-nginx) + +find_package(opentelemetry-cpp REQUIRED) +find_package(Threads REQUIRED) +find_package(protobuf REQUIRED) +find_package(gRPC REQUIRED) + +include(${CMAKE_CURRENT_SOURCE_DIR}/nginx.cmake) + +add_library(otel_ngx_module SHARED + src/nginx_config.cpp + src/toml.c + src/agent_config.cpp + src/trace_context.cpp + src/otel_ngx_module.cpp + src/otel_ngx_module_modules.c + src/propagate.cpp + src/script.cpp +) + +target_compile_options(otel_ngx_module + PRIVATE -Wall -Wextra +) + +install(TARGETS otel_ngx_module DESTINATION ".") + +# Omit the lib prefix to be more in line with nginx module naming +set_target_properties(otel_ngx_module PROPERTIES + PREFIX "" +) + +add_dependencies(otel_ngx_module project_nginx) + +target_include_directories(otel_ngx_module + PRIVATE + ${NGINX_INCLUDE_DIRS} + ${OPENTELEMETRY_CPP_INCLUDE_DIRS} +) +target_link_libraries(otel_ngx_module + PRIVATE + ${OPENTELEMETRY_CPP_LIBRARIES} + gRPC::grpc++ +) diff --git a/instrumentation/nginx/LICENSE b/instrumentation/nginx/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/instrumentation/nginx/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/instrumentation/nginx/README.md b/instrumentation/nginx/README.md new file mode 100644 index 000000000..176eacb8e --- /dev/null +++ b/instrumentation/nginx/README.md @@ -0,0 +1,154 @@ +# OpenTelemetry nginx module + +Adds OpenTelemetry distributed tracing support to nginx. + +Supported propagation types: +* [W3C](https://w3c.github.io/trace-context/) - default +* [b3](https://github.com/openzipkin/b3-propagation) + +## Requirements + +* OS: Linux +* Nginx: latest stable - [1.18.0](http://nginx.org/en/download.html) +* Nginx modules: ngx_http_upstream_module (proxy_pass), ngx_http_fastcgi_module (fastcgi_pass) + +Additional platforms and/or versions coming soon. + +## Usage + +Modify nginx.conf, or see the [example](test/conf/nginx.conf) + +``` +load_module /path/to/otel_ngx_module.so; + +http { + opentelemetry_config /conf/otel-nginx.toml; + + server { + listen 80; + server_name otel_example; + + root /var/www/html; + + location = / { + opentelemetry_operation_name my_example_backend; + opentelemetry_propagate; + proxy_pass http://localhost:3500/; + } + + location = /b3 { + opentelemetry_operation_name my_other_backend; + opentelemetry_propagate b3; + proxy_pass http://localhost:3501/; + } + + location ~ \.php$ { + root /var/www/html/php; + opentelemetry_operation_name php_fpm_backend; + opentelemetry_propagate; + fastcgi_pass localhost:9000; + include fastcgi.conf; + } + } +} + +``` + +Example [otel-nginx.toml](test/conf/otel-nginx.toml): +```toml +exporter = "otlp" +processor = "batch" + +[exporters.otlp] +host = "localhost" +port = 4317 + +[processors.batch] +max_queue_size = 2048 +schedule_delay_millis = 5000 +max_export_batch_size = 512 + +[service] +name = "nginx-proxy" # Opentelemetry resource name +``` + +## nginx directives + +### `opentelemetry` + +Enable or disable OpenTelemetry (default: enabled). + +- **required**: `false` +- **syntax**: `opentelemetry on|off` +- **block**: `http`, `server`, `location` + +### `opentelemetry_config` + +Exporters, processors + +- **required**: `true` +- **syntax**: `opentelemetry_config /path/to/config.toml` +- **block**: `http` + +### `opentelemetry_operation_name` + +Set the operation name when starting a new span. + +- **required**: `false` +- **syntax**: `opentelemetry_operation_name ` +- **block**: `http`, `server`, `location` + +### `opentelemetry_propagate` + +Enable propagation of distributed tracing headers, e.g. `traceparent`. When no parent trace is given, a new trace will +be started. The default propagator is W3C. + +- **required**: `false` +- **syntax**: `opentelemetry_propagate` or `opentelemetry_propagate b3` +- **block**: `http`, `server`, `location` + +## OpenTelemetry attributes + +List of exported attributes and their corresponding nginx variables if applicable: + +- `http.status_code` +- `http.method` +- `http.target` +- `http.flavor` +- `http.host` - `Host` header value +- `http.scheme` - `$scheme` +- `http.server_name` - From the `server_name` directive + +## Dependencies + +1. [gRPC](https://github.com/grpc/grpc) - currently the only supported exporter is OTLP. This requirement will be lifted + once more exporters become available. +2. [opentelemetry-cpp](https://github.com/open-telemetry/opentelemetry-cpp) - opentelemetry-cpp needs to be built with + position independent code and OTLP support, e.g.: +``` +cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DWITH_OTLP=ON .. +``` + +## Building + +``` +mkdir build +cd build +cmake .. +make +``` + +## Testing + +Dependencies: +* [Elixir](https://elixir-lang.org/install.html) +* [Docker](https://docs.docker.com/engine/install/) +* [Docker Compose](https://docs.docker.com/compose/install/) + +``` +cd test +docker build -t otel-nginx-test/nginx -f Dockerfile . +docker build -t otel-nginx-test/express-backend -f backend/simple_express/Dockerfile backend/simple_express +cd instrumentation +mix test +``` diff --git a/instrumentation/nginx/ci/build_module.sh b/instrumentation/nginx/ci/build_module.sh new file mode 100755 index 000000000..6aa055de3 --- /dev/null +++ b/instrumentation/nginx/ci/build_module.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +mkdir -p build +cd build +cmake .. +make -j2 diff --git a/instrumentation/nginx/ci/grpc.sh b/instrumentation/nginx/ci/grpc.sh new file mode 100755 index 000000000..420f6f336 --- /dev/null +++ b/instrumentation/nginx/ci/grpc.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +mkdir -p grpc/cmake/build +cd grpc/cmake/build +cmake -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF \ + -DgRPC_ZLIB_PROVIDER=package \ + -DgRPC_SSL_PROVIDER=package \ + -DgRPC_RE2_PROVIDER=package \ + -DgRPC_BUILD_GRPC_NODE_PLUGIN=OFF \ + -DgRPC_BUILD_GRPC_OBJECTIVE_C_PLUGIN=OFF \ + -DgRPC_BUILD_GRPC_PHP_PLUGIN=OFF \ + -DgRPC_BUILD_GRPC_PHP_PLUGIN=OFF \ + -DgRPC_BUILD_GRPC_PYTHON_PLUGIN=OFF \ + -DgRPC_BUILD_GRPC_RUBY_PLUGIN=OFF \ + -DgRPC_BUILD_CSHARP_EXTENSIONS=OFF ../.. +make -j2 +sudo make install diff --git a/instrumentation/nginx/ci/otel-cpp.sh b/instrumentation/nginx/ci/otel-cpp.sh new file mode 100755 index 000000000..772177f87 --- /dev/null +++ b/instrumentation/nginx/ci/otel-cpp.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +cd opentelemetry-cpp +mkdir build +cd build +cmake -DWITH_OTLP=ON -DBUILD_TESTING=OFF -DWITH_EXAMPLES=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON .. +make -j2 +sudo make install diff --git a/instrumentation/nginx/ci/setup_ci_environment.sh b/instrumentation/nginx/ci/setup_ci_environment.sh new file mode 100755 index 000000000..97e06bb53 --- /dev/null +++ b/instrumentation/nginx/ci/setup_ci_environment.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +export DEBIAN_FRONTEND=noninteractive +export TZ="Europe/London" + +apt-get update +apt-get install --no-install-recommends --no-install-suggests -y \ + build-essential autoconf libtool pkg-config ca-certificates \ + cmake gcc g++ libpcre3-dev zlib1g-dev libssl-dev libre2-dev \ + diff --git a/instrumentation/nginx/ci/setup_test_environment.sh b/instrumentation/nginx/ci/setup_test_environment.sh new file mode 100755 index 000000000..1767b6f63 --- /dev/null +++ b/instrumentation/nginx/ci/setup_test_environment.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +export DEBIAN_FRONTEND=noninteractive +export TZ="Europe/London" + +wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb && dpkg -i erlang-solutions_2.0_all.deb +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - +add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + +apt-get update +apt-get install --no-install-recommends --no-install-suggests -y \ + apt-transport-https ca-certificates curl gnupg-agent software-properties-common \ + python3 esl-erlang elixir docker-ce docker-ce-cli containerd.io +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + +curl -L "https://github.com/docker/compose/releases/download/1.28.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + +chmod +x /usr/local/bin/docker-compose diff --git a/instrumentation/nginx/nginx.cmake b/instrumentation/nginx/nginx.cmake new file mode 100644 index 000000000..c42a3065d --- /dev/null +++ b/instrumentation/nginx/nginx.cmake @@ -0,0 +1,23 @@ +include(ExternalProject) + +set(NGINX_VER "1.18.0") + +ExternalProject_Add(project_nginx + URL "http://nginx.org/download/nginx-${NGINX_VER}.tar.gz" + PREFIX "nginx" + BUILD_IN_SOURCE 1 + CONFIGURE_COMMAND ./configure --with-compat + BUILD_COMMAND "" + INSTALL_COMMAND "" +) + +set(NGINX_DIR "${CMAKE_BINARY_DIR}/nginx/src/project_nginx") + +set(NGINX_INCLUDE_DIRS + ${NGINX_DIR}/objs + ${NGINX_DIR}/src/core + ${NGINX_DIR}/src/os/unix + ${NGINX_DIR}/src/event + ${NGINX_DIR}/src/http + ${NGINX_DIR}/src/http/modules +) diff --git a/instrumentation/nginx/src/agent_config.cpp b/instrumentation/nginx/src/agent_config.cpp new file mode 100644 index 000000000..a0fb24d16 --- /dev/null +++ b/instrumentation/nginx/src/agent_config.cpp @@ -0,0 +1,176 @@ +#include "agent_config.h" +#include "toml.h" +#include +#include + +struct ScopedTable { + ScopedTable(toml_table_t* table) : table(table) {} + ~ScopedTable() { toml_free(table); } + + toml_table_t* table; +}; + +static std::string FromStringDatum(toml_datum_t datum) { + std::string val{datum.u.s}; + free(datum.u.s); + return val; +} + +static bool SetupOtlpExporter(toml_table_t* table, ngx_log_t* log, OtelNgxAgentConfig* config) { + toml_datum_t hostVal = toml_string_in(table, "host"); + toml_datum_t portVal = toml_int_in(table, "port"); + + if (!hostVal.ok) { + ngx_log_error(NGX_LOG_ERR, log, 0, "Missing required host field for OTLP exporter"); + return false; + } + + std::string host = FromStringDatum(hostVal); + + if (!portVal.ok) { + ngx_log_error(NGX_LOG_ERR, log, 0, "Missing required port field for OTLP exporter"); + return false; + } + + config->exporter.host = host; + config->exporter.port = portVal.u.i; + + return true; +} + +static bool SetupExporter(toml_table_t* root, ngx_log_t* log, OtelNgxAgentConfig* config) { + toml_datum_t exporterVal = toml_string_in(root, "exporter"); + + if (!exporterVal.ok) { + ngx_log_error(NGX_LOG_ERR, log, 0, "Missing required exporter field"); + return false; + } + + std::string exporter = FromStringDatum(exporterVal); + + toml_table_t* exporters = toml_table_in(root, "exporters"); + + if (!exporters) { + ngx_log_error(NGX_LOG_ERR, log, 0, "Unable to find exporters table"); + return false; + } + + if (exporter == "otlp") { + toml_table_t* otlp = toml_table_in(exporters, "otlp"); + + if (!otlp) { + ngx_log_error(NGX_LOG_ERR, log, 0, "Unable to find exporters.otlp"); + return false; + } + + if (!SetupOtlpExporter(otlp, log, config)) { + return false; + } + + config->exporter.type = OtelExporterOTLP; + } else { + ngx_log_error(NGX_LOG_ERR, log, 0, "Unsupported exporter %s", exporter.c_str()); + return false; + } + + return true; +} + +static bool SetupService(toml_table_t* root, ngx_log_t*, OtelNgxAgentConfig* config) { + toml_table_t* service = toml_table_in(root, "service"); + + if (service) { + toml_datum_t serviceName = toml_string_in(service, "name"); + + if (serviceName.ok) { + config->service.name = FromStringDatum(serviceName); + } + } + + return true; +} + +static bool SetupProcessor(toml_table_t* root, ngx_log_t* log, OtelNgxAgentConfig* config) { + toml_datum_t processorVal = toml_string_in(root, "processor"); + + if (!processorVal.ok) { + ngx_log_error(NGX_LOG_ERR, log, 0, "Unable to find required processor field"); + return false; + } + + std::string processor = FromStringDatum(processorVal); + + if (processor != "batch") { + config->processor.type = OtelProcessorSimple; + return true; + } + + config->processor.type = OtelProcessorBatch; + + toml_table_t* processors = toml_table_in(root, "processors"); + + if (!processors) { + // Go with the default batch processor config + return true; + } + + toml_table_t* batchProcessor = toml_table_in(processors, "batch"); + + if (!batchProcessor) { + return true; + } + + toml_datum_t maxQueueSize = toml_int_in(batchProcessor, "max_queue_size"); + + if (maxQueueSize.ok) { + config->processor.batch.maxQueueSize = std::max(int64_t(1), maxQueueSize.u.i); + } + + toml_datum_t scheduleDelayMillis = toml_int_in(batchProcessor, "schedule_delay_millis"); + + if (scheduleDelayMillis.ok) { + config->processor.batch.scheduleDelayMillis = std::max(int64_t(0), scheduleDelayMillis.u.i); + } + + toml_datum_t maxExportBatchSize = toml_int_in(batchProcessor, "max_export_batch_size"); + + if (maxExportBatchSize.ok) { + config->processor.batch.maxExportBatchSize = std::max(int64_t(1), maxExportBatchSize.u.i); + } + + return true; +} + +bool OtelAgentConfigLoad(const std::string& path, ngx_log_t* log, OtelNgxAgentConfig* config) { + FILE* confFile = fopen(path.c_str(), "r"); + + if (!confFile) { + ngx_log_error(NGX_LOG_ERR, log, 0, "Unable to open agent config file at %s", path.c_str()); + return false; + } + + char errBuf[256] = {0}; + ScopedTable scopedConf{toml_parse_file(confFile, errBuf, sizeof(errBuf))}; + fclose(confFile); + + if (!scopedConf.table) { + ngx_log_error(NGX_LOG_ERR, log, 0, "Configuration error: %s", errBuf); + return false; + } + + toml_table_t* root = scopedConf.table; + + if (!SetupExporter(root, log, config)) { + return false; + } + + if (!SetupService(root, log, config)) { + return false; + } + + if (!SetupProcessor(root, log, config)) { + return false; + } + + return true; +} diff --git a/instrumentation/nginx/src/agent_config.h b/instrumentation/nginx/src/agent_config.h new file mode 100644 index 000000000..0e5066345 --- /dev/null +++ b/instrumentation/nginx/src/agent_config.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +extern "C" { +#include +} + +enum OtelExporterType { OtelExporterOTLP, OtelExporterJaeger }; +enum OtelProcessorType { OtelProcessorSimple, OtelProcessorBatch }; + +struct OtelNgxAgentConfig { + struct { + OtelExporterType type = OtelExporterOTLP; + std::string host; + uint32_t port; + } exporter; + + struct { + std::string name = "unknown:nginx"; + } service; + + struct { + OtelProcessorType type = OtelProcessorSimple; + + struct { + uint32_t maxQueueSize = 2048; + uint32_t maxExportBatchSize = 512; + uint32_t scheduleDelayMillis = 5000; + } batch; + } processor; +}; + +bool OtelAgentConfigLoad(const std::string& path, ngx_log_t* log, OtelNgxAgentConfig* config); diff --git a/instrumentation/nginx/src/location_config.h b/instrumentation/nginx/src/location_config.h new file mode 100644 index 000000000..eb0dd48b5 --- /dev/null +++ b/instrumentation/nginx/src/location_config.h @@ -0,0 +1,18 @@ +#pragma once + +#include "trace_context.h" +#include "script.h" + +extern "C" { +extern ngx_module_t otel_ngx_module; +} + +struct OtelNgxLocationConf { + ngx_flag_t enabled = NGX_CONF_UNSET; + TracePropagationType propagationType = TracePropagationW3C; + NgxCompiledScript operationNameScript; +}; + +inline OtelNgxLocationConf* GetOtelLocationConf(ngx_http_request_t* req) { + return (OtelNgxLocationConf*)ngx_http_get_module_loc_conf(req, otel_ngx_module); +} diff --git a/instrumentation/nginx/src/nginx_config.cpp b/instrumentation/nginx/src/nginx_config.cpp new file mode 100644 index 000000000..fe75a49f1 --- /dev/null +++ b/instrumentation/nginx/src/nginx_config.cpp @@ -0,0 +1,139 @@ +#include "nginx_config.h" + +static const ngx_uint_t argument_number[] = { + NGX_CONF_NOARGS, NGX_CONF_TAKE1, NGX_CONF_TAKE2, NGX_CONF_TAKE3, + NGX_CONF_TAKE4, NGX_CONF_TAKE5, NGX_CONF_TAKE6, NGX_CONF_TAKE7, +}; + +ngx_int_t OtelNgxConfHandler(ngx_conf_t* cf, ngx_int_t last) { + void *conf, **confp; + + ngx_str_t* name = (ngx_str_t*)cf->args->elts; + + ngx_uint_t found = 0; + + for (ngx_uint_t i = 0; cf->cycle->modules[i]; i++) { + + ngx_command_t* cmd = cf->cycle->modules[i]->commands; + if (cmd == NULL) { + continue; + } + + for (/* void */; cmd->name.len; cmd++) { + + if (name->len != cmd->name.len) { + continue; + } + + if (ngx_strcmp(name->data, cmd->name.data) != 0) { + continue; + } + + found = 1; + + if ( + cf->cycle->modules[i]->type != NGX_CONF_MODULE && + cf->cycle->modules[i]->type != cf->module_type) { + continue; + } + + /* is the directive's location right ? */ + + if (!(cmd->type & cf->cmd_type)) { + continue; + } + + if (!(cmd->type & NGX_CONF_BLOCK) && last != NGX_OK) { + ngx_conf_log_error( + NGX_LOG_EMERG, cf, 0, "directive \"%s\" is not terminated by \";\"", name->data); + return NGX_ERROR; + } + + if ((cmd->type & NGX_CONF_BLOCK) && last != NGX_CONF_BLOCK_START) { + ngx_conf_log_error( + NGX_LOG_EMERG, cf, 0, "directive \"%s\" has no opening \"{\"", name->data); + return NGX_ERROR; + } + + /* is the directive's argument count right ? */ + + if (!(cmd->type & NGX_CONF_ANY)) { + + if (cmd->type & NGX_CONF_FLAG) { + + if (cf->args->nelts != 2) { + goto invalid; + } + + } else if (cmd->type & NGX_CONF_1MORE) { + + if (cf->args->nelts < 2) { + goto invalid; + } + + } else if (cmd->type & NGX_CONF_2MORE) { + + if (cf->args->nelts < 3) { + goto invalid; + } + + } else if (cf->args->nelts > NGX_CONF_MAX_ARGS) { + + goto invalid; + + } else if (!(cmd->type & argument_number[cf->args->nelts - 1])) { + goto invalid; + } + } + + /* set up the directive's configuration context */ + + conf = NULL; + + if (cmd->type & NGX_DIRECT_CONF) { + conf = ((void**)cf->ctx)[cf->cycle->modules[i]->index]; + + } else if (cmd->type & NGX_MAIN_CONF) { + conf = &(((void**)cf->ctx)[cf->cycle->modules[i]->index]); + + } else if (cf->ctx) { + confp = (void**)*(void**)((char*)cf->ctx + cmd->conf); + + if (confp) { + conf = confp[cf->cycle->modules[i]->ctx_index]; + } + } + + char* rv = cmd->set(cf, cmd, conf); + + if (rv == NGX_CONF_OK) { + return NGX_OK; + } + + if (rv == NGX_CONF_ERROR) { + return NGX_ERROR; + } + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "\"%s\" directive %s", name->data, rv); + + return NGX_ERROR; + } + } + + if (found) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "\"%s\" directive is not allowed here", name->data); + + return NGX_ERROR; + } + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "unknown directive \"%s\"", name->data); + + return NGX_ERROR; + +invalid: + + ngx_conf_log_error( + NGX_LOG_EMERG, cf, 0, "invalid number of arguments in \"%s\" directive", name->data); + + return NGX_ERROR; +} diff --git a/instrumentation/nginx/src/nginx_config.h b/instrumentation/nginx/src/nginx_config.h new file mode 100644 index 000000000..bb9fb2730 --- /dev/null +++ b/instrumentation/nginx/src/nginx_config.h @@ -0,0 +1,7 @@ +#pragma once + +extern "C" { +#include +} + +ngx_int_t OtelNgxConfHandler(ngx_conf_t* conf, ngx_int_t last); diff --git a/instrumentation/nginx/src/nginx_utils.h b/instrumentation/nginx/src/nginx_utils.h new file mode 100644 index 000000000..99860c0df --- /dev/null +++ b/instrumentation/nginx/src/nginx_utils.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +extern "C" { +#include +} + +inline opentelemetry::nostd::string_view FromNgxString(ngx_str_t str) { + return {(const char*)str.data, str.len}; +} +inline ngx_str_t ToNgxString(opentelemetry::nostd::string_view str) { + return {str.size(), (u_char*)str.data()}; +} diff --git a/instrumentation/nginx/src/otel_ngx_module.cpp b/instrumentation/nginx/src/otel_ngx_module.cpp new file mode 100644 index 000000000..580060194 --- /dev/null +++ b/instrumentation/nginx/src/otel_ngx_module.cpp @@ -0,0 +1,611 @@ +#include +#include +#include +#include + +extern "C" { +#include +#include +#include + +extern ngx_module_t otel_ngx_module; +} + +#include "agent_config.h" +#include "location_config.h" +#include "nginx_config.h" +#include "nginx_utils.h" +#include "propagate.h" +#include +#include +#include +#include +#include +#include +#include + +namespace trace = opentelemetry::trace; +namespace nostd = opentelemetry::nostd; +namespace sdktrace = opentelemetry::sdk::trace; +namespace otlp = opentelemetry::exporter::otlp; + +struct OtelNgxScriptAttribute { + OtelNgxScriptAttribute(nostd::string_view attribute, nostd::string_view script) + : attribute(ToNgxString(attribute)), script(ToNgxString(script)) {} + ngx_str_t attribute; + ngx_str_t script; +}; + +constexpr char kOtelCtxVarPrefix[] = "otel_ctxvar_"; + +const OtelNgxScriptAttribute kDefaultScriptAttributes[] = { + {"http.scheme", "$scheme"}, +}; + +struct OtelMainConf { + ngx_array_t* scriptAttributes; + OtelNgxAgentConfig agentConfig; +}; + +struct ScriptAttribute { + NgxCompiledScript key; + NgxCompiledScript value; +}; + +nostd::shared_ptr GetTracer() { + return trace::Provider::GetTracerProvider()->GetTracer("nginx"); +} + +nostd::string_view NgxHttpFlavor(ngx_http_request_t* req) { + switch (req->http_version) { + case NGX_HTTP_VERSION_11: + return "1.1"; + case NGX_HTTP_VERSION_20: + return "2.0"; + case NGX_HTTP_VERSION_10: + return "1.0"; + default: + return ""; + } +} + +static ngx_int_t OtelGetContextVar(ngx_http_request_t*, ngx_http_variable_value_t*, uintptr_t) { + // Filled out on cotext creation. + return NGX_OK; +} + +static ngx_int_t +OtelGetTraceContextVar(ngx_http_request_t* req, ngx_http_variable_value_t* v, uintptr_t data); + +static ngx_http_variable_t otel_ngx_variables[] = { + { + ngx_string("otel_ctx"), + nullptr, + OtelGetContextVar, + 0, + NGX_HTTP_VAR_NOCACHEABLE | NGX_HTTP_VAR_NOHASH, + 0, + }, + { + ngx_string(kOtelCtxVarPrefix), + nullptr, + OtelGetTraceContextVar, + 0, + NGX_HTTP_VAR_PREFIX | NGX_HTTP_VAR_NOHASH | NGX_HTTP_VAR_NOCACHEABLE, + 0, + }, + ngx_http_null_variable, +}; + +TraceContext* GetTraceContext(ngx_http_request_t* req) { + ngx_http_variable_value_t* val = ngx_http_get_indexed_variable(req, otel_ngx_variables[0].index); + + if (val == nullptr || val->not_found) { + ngx_log_error(NGX_LOG_ERR, req->connection->log, 0, "TraceContext not found"); + return nullptr; + } + + return (TraceContext*)val->data; +} + +nostd::string_view WithoutOtelVarPrefix(ngx_str_t value) { + const size_t prefixLength = sizeof(kOtelCtxVarPrefix) - 1; + + if (value.len <= prefixLength) { + return ""; + } + + return {(const char*)value.data + prefixLength, value.len - prefixLength}; +} + +static ngx_int_t +OtelGetTraceContextVar(ngx_http_request_t* req, ngx_http_variable_value_t* v, uintptr_t data) { + TraceContext* traceContext = GetTraceContext(req); + + if (traceContext == nullptr || !traceContext->request_span) { + ngx_log_error( + NGX_LOG_ERR, req->connection->log, 0, + "Unable to get trace context when expanding tracecontext var"); + return NGX_OK; + } + + ngx_str_t* prefixedKey = (ngx_str_t*)data; + + nostd::string_view key = WithoutOtelVarPrefix(*prefixedKey); + + const TraceHeader* header = TraceContextFindTraceHeader(traceContext, key); + + if (header) { + v->len = header->value.len; + v->valid = 1; + v->no_cacheable = 1; + v->not_found = 0; + v->data = header->value.data; + } else { + v->len = 0; + v->valid = 0; + v->not_found = 1; + v->no_cacheable = 1; + v->data = nullptr; + } + + return NGX_OK; +} + +void TraceContextCleanup(void* data) { + TraceContext* context = (TraceContext*)data; + context->~TraceContext(); +} + +nostd::string_view GetOperationName(ngx_http_request_t* req) { + OtelNgxLocationConf* locationConf = GetOtelLocationConf(req); + + ngx_str_t opName = ngx_null_string; + if (locationConf->operationNameScript.Run(req, &opName)) { + return FromNgxString(opName); + } + + ngx_http_core_loc_conf_t* httpCoreLocationConf = + (ngx_http_core_loc_conf_t*)ngx_http_get_module_loc_conf(req, ngx_http_core_module); + + if (httpCoreLocationConf) { + return FromNgxString(httpCoreLocationConf->name); + } + + return FromNgxString(opName); +} + +ngx_http_core_main_conf_t* NgxHttpModuleMainConf(ngx_http_request_t* req) { + return (ngx_http_core_main_conf_t*)ngx_http_get_module_main_conf(req, ngx_http_core_module); +} + +OtelMainConf* GetOtelMainConf(ngx_http_request_t* req) { + return (OtelMainConf*)ngx_http_get_module_main_conf(req, otel_ngx_module); +} + +nostd::string_view GetNgxServerName(const ngx_http_request_t* req) { + ngx_http_core_srv_conf_t* cscf = + (ngx_http_core_srv_conf_t*)ngx_http_get_module_srv_conf(req, ngx_http_core_module); + return FromNgxString(cscf->server_name); +} + +static bool IsOtelEnabled(ngx_http_request_t* req) { + OtelNgxLocationConf* locConf = GetOtelLocationConf(req); + return locConf->enabled; +} + +ngx_int_t StartNgxSpan(ngx_http_request_t* req) { + if (!IsOtelEnabled(req)) { + return NGX_DECLINED; + } + + ngx_http_variable_value_t* val = ngx_http_get_indexed_variable(req, otel_ngx_variables[0].index); + + if (!val) { + ngx_log_error(NGX_LOG_ERR, req->connection->log, 0, "Unable to find OpenTelemetry context"); + return NGX_DECLINED; + } + + ngx_pool_cleanup_t* cleanup = ngx_pool_cleanup_add(req->pool, sizeof(TraceContext)); + TraceContext* context = (TraceContext*)cleanup->data; + new (context) TraceContext(req); + cleanup->handler = TraceContextCleanup; + + val->data = (unsigned char*)cleanup->data; + val->len = sizeof(TraceContext); + + OtelCarrier carrier{req, context}; + auto incomingContext = ExtractContext(&carrier); + + trace::StartSpanOptions startOpts; + startOpts.kind = trace::SpanKind::kServer; + startOpts.parent = GetCurrentSpan(incomingContext); + + context->request_span = GetTracer()->StartSpan( + GetOperationName(req), + { + {"http.method", FromNgxString(req->method_name)}, + {"http.flavor", NgxHttpFlavor(req)}, + {"http.target", FromNgxString(req->unparsed_uri)}, + {"http.host", FromNgxString(req->headers_in.host->value)}, + }, + startOpts); + + nostd::string_view serverName = GetNgxServerName(req); + if (!serverName.empty()) { + context->request_span->SetAttribute("http.server_name", serverName); + } + + auto outgoingContext = incomingContext.SetValue(trace::kSpanKey, context->request_span); + + InjectContext(&carrier, outgoingContext); + + return NGX_DECLINED; +} + +void AddScriptAttributes( + trace::Span* span, const ngx_array_t* attributes, ngx_http_request_t* req) { + ScriptAttribute* elements = (ScriptAttribute*)attributes->elts; + for (ngx_uint_t i = 0; i < attributes->nelts; i++) { + ScriptAttribute* attribute = &elements[i]; + ngx_str_t key = ngx_null_string; + ngx_str_t value = ngx_null_string; + + if (attribute->key.Run(req, &key) && attribute->value.Run(req, &value)) { + span->SetAttribute(FromNgxString(key), FromNgxString(value)); + } + } +} + +ngx_int_t FinishNgxSpan(ngx_http_request_t* req) { + if (!IsOtelEnabled(req)) { + return NGX_DECLINED; + } + + TraceContext* context = GetTraceContext(req); + + if (!context) { + return NGX_DECLINED; + } + + auto span = context->request_span; + span->SetAttribute("http.status_code", req->headers_out.status); + + AddScriptAttributes(span.get(), GetOtelMainConf(req)->scriptAttributes, req); + + span->UpdateName(GetOperationName(req)); + + span->End(); + return NGX_DECLINED; +} + +bool RegisterScriptAttribute( + ngx_conf_t* conf, ngx_array_t* attributes, OtelNgxScriptAttribute attrib) { + ScriptAttribute* scriptAttrib = (ScriptAttribute*)ngx_array_push(attributes); + + if (scriptAttrib == nullptr) { + return false; + } + + new (scriptAttrib) ScriptAttribute(); + + if (!CompileScript(conf, attrib.attribute, &scriptAttrib->key)) { + return false; + } + + return CompileScript(conf, attrib.script, &scriptAttrib->value); +} + +static ngx_int_t InitModule(ngx_conf_t* conf) { + ngx_http_core_main_conf_t* main_conf = + (ngx_http_core_main_conf_t*)ngx_http_conf_get_module_main_conf(conf, ngx_http_core_module); + + struct PhaseHandler { + ngx_http_phases phase; + ngx_http_handler_pt handler; + }; + + const PhaseHandler handlers[] = { + {NGX_HTTP_REWRITE_PHASE, StartNgxSpan}, + {NGX_HTTP_LOG_PHASE, FinishNgxSpan}, + }; + + for (const PhaseHandler& ph : handlers) { + ngx_http_handler_pt* ngx_handler = + (ngx_http_handler_pt*)ngx_array_push(&main_conf->phases[ph.phase].handlers); + + if (ngx_handler == nullptr) { + continue; + } + + *ngx_handler = ph.handler; + } + + OtelMainConf* otelMainConf = + (OtelMainConf*)ngx_http_conf_get_module_main_conf(conf, otel_ngx_module); + + if (!otelMainConf) { + return NGX_ERROR; + } + + otelMainConf->scriptAttributes = ngx_array_create( + conf->pool, sizeof(kDefaultScriptAttributes) / sizeof(kDefaultScriptAttributes[0]), + sizeof(ScriptAttribute)); + + if (otelMainConf->scriptAttributes == nullptr) { + return NGX_ERROR; + } + + for (const auto& scriptAttribute : kDefaultScriptAttributes) { + if (!RegisterScriptAttribute(conf, otelMainConf->scriptAttributes, scriptAttribute)) { + return NGX_ERROR; + } + } + + return NGX_OK; +} + +static void* CreateOtelMainConf(ngx_conf_t* conf) { + OtelMainConf* mainConf = (OtelMainConf*)ngx_pcalloc(conf->pool, sizeof(OtelMainConf)); + new (mainConf) OtelMainConf(); + + return mainConf; +} + +static void* CreateOtelLocConf(ngx_conf_t* conf) { + OtelNgxLocationConf* locConf = + (OtelNgxLocationConf*)ngx_pcalloc(conf->pool, sizeof(OtelNgxLocationConf)); + new (locConf) OtelNgxLocationConf(); + return locConf; +} + +static char* MergeLocConf(ngx_conf_t*, void* parent, void* child) { + OtelNgxLocationConf* prev = (OtelNgxLocationConf*)parent; + OtelNgxLocationConf* conf = (OtelNgxLocationConf*)child; + + ngx_conf_merge_value(conf->enabled, prev->enabled, 1); + + return NGX_CONF_OK; +} + +static ngx_int_t CreateOtelNgxVariables(ngx_conf_t* conf) { + for (ngx_http_variable_t* v = otel_ngx_variables; v->name.len; v++) { + ngx_http_variable_t* var = ngx_http_add_variable(conf, &v->name, v->flags); + + if (var == nullptr) { + return NGX_ERROR; + } + + var->get_handler = v->get_handler; + var->set_handler = v->set_handler; + var->data = v->data; + v->index = var->index = ngx_http_get_variable_index(conf, &v->name); + } + + return NGX_OK; +} + +static ngx_http_module_t otel_ngx_http_module = { + CreateOtelNgxVariables, /* preconfiguration */ + InitModule, /* postconfiguration */ + CreateOtelMainConf, /* create main conf */ + nullptr, /* init main conf */ + nullptr, /* create server conf */ + nullptr, /* merge server conf */ + CreateOtelLocConf, /* create loc conf */ + MergeLocConf, /* merge loc conf */ +}; + +bool CompileCommandScript(ngx_conf_t* ngxConf, ngx_command_t*, NgxCompiledScript* script) { + ngx_str_t* value = (ngx_str_t*)ngxConf->args->elts; + ngx_str_t* pattern = &value[1]; + + return CompileScript(ngxConf, *pattern, script); +} + +char* OtelNgxSetOperationNameVar(ngx_conf_t* ngxConf, ngx_command_t* cmd, void* conf) { + auto locationConf = (OtelNgxLocationConf*)conf; + if (CompileCommandScript(ngxConf, cmd, &locationConf->operationNameScript)) { + return NGX_CONF_OK; + } + + return (char*)NGX_CONF_ERROR; +} + +struct HeaderPropagation { + nostd::string_view directive; + nostd::string_view parameter; + nostd::string_view variable; +}; + +std::vector B3PropagationVars() { + return { + {"proxy_set_header", "b3", "$otel_ctxvar_b3"}, + {"fastcgi_param", "HTTP_B3", "$otel_ctxvar_b3"}, + }; +} + +std::vector OtelPropagationVars() { + return { + {"proxy_set_header", "traceparent", "$otel_ctxvar_traceparent"}, + {"proxy_set_header", "tracestate", "$otel_ctxvar_tracestate"}, + {"fastcgi_param", "HTTP_TRACEPARENT", "$otel_ctxvar_traceparent"}, + {"fastcgi_param", "HTTP_TRACESTATE", "$otel_ctxvar_tracestate"}, + }; +} + +char* OtelNgxSetPropagation(ngx_conf_t* conf, ngx_command_t*, void* locConf) { + uint32_t numArgs = conf->args->nelts; + + auto locationConf = (OtelNgxLocationConf*)locConf; + + if (numArgs == 2) { + ngx_str_t* args = (ngx_str_t*)conf->args->elts; + nostd::string_view propagationType = FromNgxString(args[1]); + + if (propagationType == "b3") { + locationConf->propagationType = TracePropagationB3; + } else if (propagationType == "w3c") { + locationConf->propagationType = TracePropagationW3C; + } else { + ngx_log_error(NGX_LOG_ERR, conf->log, 0, "Unsupported propagation type"); + return (char*)NGX_CONF_ERROR; + } + } + + std::vector propagationVars; + if (locationConf->propagationType == TracePropagationB3) { + propagationVars = B3PropagationVars(); + } else { + propagationVars = OtelPropagationVars(); + } + + ngx_array_t* oldArgs = conf->args; + + for (const HeaderPropagation& propagation : propagationVars) { + ngx_str_t args[] = { + ToNgxString(propagation.directive), + ToNgxString(propagation.parameter), + ToNgxString(propagation.variable), + }; + + ngx_array_t argsArray; + ngx_memzero(&argsArray, sizeof(argsArray)); + + argsArray.elts = &args; + argsArray.nelts = 3; + conf->args = &argsArray; + + if (OtelNgxConfHandler(conf, 0) != NGX_OK) { + conf->args = oldArgs; + return (char*)NGX_CONF_ERROR; + } + } + + conf->args = oldArgs; + + return NGX_CONF_OK; +} + +char* OtelNgxSetConfig(ngx_conf_t* conf, ngx_command_t*, void*) { + OtelMainConf* mainConf = (OtelMainConf*)ngx_http_conf_get_module_main_conf(conf, otel_ngx_module); + + ngx_str_t* values = (ngx_str_t*)conf->args->elts; + ngx_str_t* path = &values[1]; + + if (!OtelAgentConfigLoad( + std::string((const char*)path->data, path->len), conf->log, &mainConf->agentConfig)) { + return (char*)NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + +static ngx_command_t kOtelNgxCommands[] = { + { + ngx_string("opentelemetry_propagate"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS | NGX_CONF_TAKE1, + OtelNgxSetPropagation, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + nullptr, + }, + { + ngx_string("opentelemetry_operation_name"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + OtelNgxSetOperationNameVar, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + nullptr, + }, + { + ngx_string("opentelemetry_config"), + NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE1, + OtelNgxSetConfig, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + nullptr, + }, + { + ngx_string("opentelemetry"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(OtelNgxLocationConf, enabled), + nullptr, + }, + ngx_null_command, +}; + +static std::unique_ptr CreateExporter(const OtelNgxAgentConfig* conf) { + std::unique_ptr exporter; + + switch (conf->exporter.type) { + case OtelExporterOTLP: { + std::string endpoint = conf->exporter.host + ":" + std::to_string(conf->exporter.port); + otlp::OtlpExporterOptions opts{endpoint}; + exporter.reset(new otlp::OtlpExporter(opts)); + break; + } + default: + break; + } + + return exporter; +} + +static std::shared_ptr +CreateProcessor(const OtelNgxAgentConfig* conf, std::unique_ptr exporter) { + if (conf->processor.type == OtelProcessorBatch) { + sdktrace::BatchSpanProcessorOptions opts; + opts.max_queue_size = conf->processor.batch.maxQueueSize; + opts.schedule_delay_millis = + std::chrono::milliseconds(conf->processor.batch.scheduleDelayMillis); + opts.max_export_batch_size = conf->processor.batch.maxExportBatchSize; + + return std::shared_ptr( + new sdktrace::BatchSpanProcessor(std::move(exporter), opts)); + } + + return std::shared_ptr( + new sdktrace::SimpleSpanProcessor(std::move(exporter))); +} + +static ngx_int_t OtelNgxStart(ngx_cycle_t* cycle) { + OtelMainConf* otelMainConf = + (OtelMainConf*)ngx_http_cycle_get_module_main_conf(cycle, otel_ngx_module); + + OtelNgxAgentConfig* agentConf = &otelMainConf->agentConfig; + + auto exporter = CreateExporter(agentConf); + + if (!exporter) { + ngx_log_error(NGX_LOG_ERR, cycle->log, 0, "Unable to create span exporter - invalid type"); + return NGX_ERROR; + } + + auto processor = CreateProcessor(agentConf, std::move(exporter)); + + auto provider = nostd::shared_ptr(new sdktrace::TracerProvider( + processor, + opentelemetry::sdk::resource::Resource::Create({{"service.name", agentConf->service.name}}))); + + trace::Provider::SetTracerProvider(provider); + + return NGX_OK; +} + +ngx_module_t otel_ngx_module = { + NGX_MODULE_V1, + &otel_ngx_http_module, + kOtelNgxCommands, + NGX_HTTP_MODULE, + nullptr, /* init master */ + nullptr, /* init module - prior to forking from master process */ + OtelNgxStart, /* init process - worker process fork */ + nullptr, /* init thread */ + nullptr, /* exit thread */ + nullptr, /* exit process - worker process exit */ + nullptr, /* exit master */ + NGX_MODULE_V1_PADDING, +}; diff --git a/instrumentation/nginx/src/otel_ngx_module_modules.c b/instrumentation/nginx/src/otel_ngx_module_modules.c new file mode 100644 index 000000000..643ca9d9d --- /dev/null +++ b/instrumentation/nginx/src/otel_ngx_module_modules.c @@ -0,0 +1,18 @@ +#include +#include + +extern ngx_module_t otel_ngx_module; + +ngx_module_t* ngx_modules[] = { + &otel_ngx_module, + NULL, +}; + +char* ngx_module_names[] = { + "otel_ngx_module", + NULL, +}; + +char* ngx_module_order[] = { + NULL, +}; diff --git a/instrumentation/nginx/src/propagate.cpp b/instrumentation/nginx/src/propagate.cpp new file mode 100644 index 000000000..43694c253 --- /dev/null +++ b/instrumentation/nginx/src/propagate.cpp @@ -0,0 +1,109 @@ +#include "propagate.h" +#include "location_config.h" +#include "nginx_utils.h" +#include +#include +#include +#include +#include + +namespace trace = opentelemetry::trace; +namespace nostd = opentelemetry::nostd; + +using OtelB3Propagator = trace::propagation::B3Propagator; +using OtelB3MultiPropagator = trace::propagation::B3PropagatorMultiHeader; +using OtelW3CPropagator = trace::propagation::HttpTraceContext; + +static bool FindHeader(ngx_http_request_t* req, nostd::string_view key, nostd::string_view* value) { + ngx_list_part_t* part = &req->headers_in.headers.part; + ngx_table_elt_t* h = (ngx_table_elt_t*)part->elts; + + for (ngx_uint_t i = 0;; i++) { + if (i >= part->nelts) { + if (part->next == nullptr) { + break; + } + + part = part->next; + h = (ngx_table_elt_t*)part->elts; + i = 0; + } + + if ( + key.size() != h[i].key.len || + ngx_strncasecmp((u_char*)key.data(), h[i].key.data, key.length()) != 0) { + continue; + } + + *value = FromNgxString(h[i].value); + return true; + } + + return false; +} + +static void OtelPopulateCarrier( + OtelCarrier& carrier, nostd::string_view traceType, nostd::string_view traceValue) { + TraceContextSetTraceHeader(carrier.traceContext, traceType, traceValue); +} + +static nostd::string_view OtelGetReqHeader(const OtelCarrier& carrier, nostd::string_view key) { + nostd::string_view value; + FindHeader(carrier.req, key, &value); + return value; +} + +static bool HasHeader(ngx_http_request_t* req, nostd::string_view header) { + nostd::string_view value; + return FindHeader(req, header, &value); +} + +TracePropagationType GetPropagationType(ngx_http_request_t* req) { + OtelNgxLocationConf* config = GetOtelLocationConf(req); + return config->propagationType; +} + +opentelemetry::context::Context ExtractContext(OtelCarrier* carrier) { + TracePropagationType propagationType = GetPropagationType(carrier->req); + + opentelemetry::context::Context root; + switch (propagationType) { + case TracePropagationW3C: { + return OtelW3CPropagator().Extract(OtelGetReqHeader, *carrier, root); + } + case TracePropagationB3: { + if (HasHeader(carrier->req, "b3")) { + return OtelB3Propagator().Extract(OtelGetReqHeader, *carrier, root); + } + + return OtelB3MultiPropagator().Extract(OtelGetReqHeader, *carrier, root); + } + default: + return root; + } +} + +void InjectContext(OtelCarrier* carrier, opentelemetry::context::Context context) { + TracePropagationType propagationType = GetPropagationType(carrier->req); + switch (propagationType) { + case TracePropagationW3C: { + OtelW3CPropagator().Inject(OtelPopulateCarrier, *carrier, context); + break; + } + case TracePropagationB3: { + OtelB3Propagator().Inject(OtelPopulateCarrier, *carrier, context); + break; + } + default: + break; + } +} + +opentelemetry::trace::SpanContext GetCurrentSpan(opentelemetry::context::Context context) { + opentelemetry::context::ContextValue span = context.GetValue(trace::kSpanKey); + if (nostd::holds_alternative>(span)) { + return nostd::get>(span).get()->GetContext(); + } + + return trace::SpanContext::GetInvalid(); +} diff --git a/instrumentation/nginx/src/propagate.h b/instrumentation/nginx/src/propagate.h new file mode 100644 index 000000000..f116d909d --- /dev/null +++ b/instrumentation/nginx/src/propagate.h @@ -0,0 +1,13 @@ +#pragma once + +#include "trace_context.h" +#include + +struct OtelCarrier { + ngx_http_request_t* req; + TraceContext* traceContext; +}; + +opentelemetry::context::Context ExtractContext(OtelCarrier* carrier); +void InjectContext(OtelCarrier* carrier, opentelemetry::context::Context context); +opentelemetry::trace::SpanContext GetCurrentSpan(opentelemetry::context::Context context); diff --git a/instrumentation/nginx/src/script.cpp b/instrumentation/nginx/src/script.cpp new file mode 100644 index 000000000..25ebe3b69 --- /dev/null +++ b/instrumentation/nginx/src/script.cpp @@ -0,0 +1,26 @@ +#include "script.h" + +bool CompileScript(ngx_conf_t* conf, ngx_str_t pattern, NgxCompiledScript* script) { + script->pattern = pattern; + script->lengths = nullptr; + script->values = nullptr; + + ngx_uint_t numVariables = ngx_http_script_variables_count(&script->pattern); + + if (numVariables == 0) { + return true; + } + + ngx_http_script_compile_t compilation; + ngx_memzero(&compilation, sizeof(compilation)); + compilation.cf = conf; + compilation.source = &script->pattern; + compilation.lengths = &script->lengths; + compilation.values = &script->values; + compilation.variables = numVariables; + compilation.complete_lengths = 1; + compilation.complete_values = 1; + + return ngx_http_script_compile(&compilation) == NGX_OK; +} + diff --git a/instrumentation/nginx/src/script.h b/instrumentation/nginx/src/script.h new file mode 100644 index 000000000..8aa4fda03 --- /dev/null +++ b/instrumentation/nginx/src/script.h @@ -0,0 +1,30 @@ +#pragma once + +extern "C" { +#include +#include +#include +} + +struct NgxCompiledScript { + ngx_str_t pattern = ngx_null_string; + ngx_array_t* lengths = nullptr; + ngx_array_t* values = nullptr; + + bool Run(ngx_http_request_t* req, ngx_str_t* result) { + if (!lengths) { + *result = pattern; + return true; + } + + if (!ngx_http_script_run(req, result, lengths->elts, 0, values->elts)) { + *result = ngx_null_string; + return false; + } + + return true; + } +}; + +bool CompileScript(ngx_conf_t* conf, ngx_str_t pattern, NgxCompiledScript* script); + diff --git a/instrumentation/nginx/src/toml.c b/instrumentation/nginx/src/toml.c new file mode 100644 index 000000000..bb899d5d4 --- /dev/null +++ b/instrumentation/nginx/src/toml.c @@ -0,0 +1,2247 @@ +/* + + MIT License + + Copyright (c) 2017 - 2019 CK Tan + https://github.com/cktan/tomlc99 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +*/ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include "toml.h" + + +static void* (*ppmalloc)(size_t) = malloc; +static void (*ppfree)(void*) = free; + +void toml_set_memutil(void* (*xxmalloc)(size_t), + void (*xxfree)(void*)) +{ + if (xxmalloc) ppmalloc = xxmalloc; + if (xxfree) ppfree = xxfree; +} + + +#define MALLOC(a) ppmalloc(a) +#define FREE(a) ppfree(a) + +static void* CALLOC(size_t nmemb, size_t sz) +{ + int nb = sz * nmemb; + void* p = MALLOC(nb); + if (p) { + memset(p, 0, nb); + } + return p; +} + + +static char* STRDUP(const char* s) +{ + int len = strlen(s); + char* p = MALLOC(len+1); + if (p) { + memcpy(p, s, len); + p[len] = 0; + } + return p; +} + +static char* STRNDUP(const char* s, size_t n) +{ + size_t len = strnlen(s, n); + char* p = MALLOC(len+1); + if (p) { + memcpy(p, s, len); + p[len] = 0; + } + return p; +} + + + +/** + * Convert a char in utf8 into UCS, and store it in *ret. + * Return #bytes consumed or -1 on failure. + */ +int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret) +{ + const unsigned char* buf = (const unsigned char*) orig; + unsigned i = *buf++; + int64_t v; + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (0 == (i >> 7)) { + if (len < 1) return -1; + v = i; + return *ret = v, 1; + } + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (0x6 == (i >> 5)) { + if (len < 2) return -1; + v = i & 0x1f; + for (int j = 0; j < 1; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (0xE == (i >> 4)) { + if (len < 3) return -1; + v = i & 0x0F; + for (int j = 0; j < 2; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x1E == (i >> 3)) { + if (len < 4) return -1; + v = i & 0x07; + for (int j = 0; j < 3; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x3E == (i >> 2)) { + if (len < 5) return -1; + v = i & 0x03; + for (int j = 0; j < 4; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x7e == (i >> 1)) { + if (len < 6) return -1; + v = i & 0x01; + for (int j = 0; j < 5; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + return -1; +} + + +/** + * Convert a UCS char to utf8 code, and return it in buf. + * Return #bytes used in buf to encode the char, or + * -1 on error. + */ +int toml_ucs_to_utf8(int64_t code, char buf[6]) +{ + /* http://stackoverflow.com/questions/6240055/manually-converting-unicode-codepoints-into-utf-8-and-utf-16 */ + /* The UCS code values 0xd800–0xdfff (UTF-16 surrogates) as well + * as 0xfffe and 0xffff (UCS noncharacters) should not appear in + * conforming UTF-8 streams. + */ + if (0xd800 <= code && code <= 0xdfff) return -1; + if (0xfffe <= code && code <= 0xffff) return -1; + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (code < 0) return -1; + if (code <= 0x7F) { + buf[0] = (unsigned char) code; + return 1; + } + + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (code <= 0x000007FF) { + buf[0] = 0xc0 | (code >> 6); + buf[1] = 0x80 | (code & 0x3f); + return 2; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x0000FFFF) { + buf[0] = 0xe0 | (code >> 12); + buf[1] = 0x80 | ((code >> 6) & 0x3f); + buf[2] = 0x80 | (code & 0x3f); + return 3; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x001FFFFF) { + buf[0] = 0xf0 | (code >> 18); + buf[1] = 0x80 | ((code >> 12) & 0x3f); + buf[2] = 0x80 | ((code >> 6) & 0x3f); + buf[3] = 0x80 | (code & 0x3f); + return 4; + } + + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x03FFFFFF) { + buf[0] = 0xf8 | (code >> 24); + buf[1] = 0x80 | ((code >> 18) & 0x3f); + buf[2] = 0x80 | ((code >> 12) & 0x3f); + buf[3] = 0x80 | ((code >> 6) & 0x3f); + buf[4] = 0x80 | (code & 0x3f); + return 5; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x7FFFFFFF) { + buf[0] = 0xfc | (code >> 30); + buf[1] = 0x80 | ((code >> 24) & 0x3f); + buf[2] = 0x80 | ((code >> 18) & 0x3f); + buf[3] = 0x80 | ((code >> 12) & 0x3f); + buf[4] = 0x80 | ((code >> 6) & 0x3f); + buf[5] = 0x80 | (code & 0x3f); + return 6; + } + + return -1; +} + +/* + * TOML has 3 data structures: value, array, table. + * Each of them can have identification key. + */ +typedef struct toml_keyval_t toml_keyval_t; +struct toml_keyval_t { + const char* key; /* key to this value */ + const char* val; /* the raw value */ +}; + + +struct toml_array_t { + const char* key; /* key to this array */ + int kind; /* element kind: 'v'alue, 'a'rray, or 't'able */ + int type; /* for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime, 'D'ate, 'T'imestamp */ + + int nelem; /* number of elements */ + union { + char** val; + toml_array_t** arr; + toml_table_t** tab; + } u; +}; + + +struct toml_table_t { + const char* key; /* key to this table */ + bool implicit; /* table was created implicitly */ + + /* key-values in the table */ + int nkval; + toml_keyval_t** kval; + + /* arrays in the table */ + int narr; + toml_array_t** arr; + + /* tables in the table */ + int ntab; + toml_table_t** tab; +}; + + +static inline void xfree(const void* x) { if (x) FREE((void*)(intptr_t)x); } + + +enum tokentype_t { + INVALID, + DOT, + COMMA, + EQUAL, + LBRACE, + RBRACE, + NEWLINE, + LBRACKET, + RBRACKET, + STRING, +}; +typedef enum tokentype_t tokentype_t; + +typedef struct token_t token_t; +struct token_t { + tokentype_t tok; + int lineno; + char* ptr; /* points into context->start */ + int len; + int eof; +}; + + +typedef struct context_t context_t; +struct context_t { + char* start; + char* stop; + char* errbuf; + int errbufsz; + + token_t tok; + toml_table_t* root; + toml_table_t* curtab; + + struct { + int top; + char* key[10]; + token_t tok[10]; + } tpath; + +}; + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) +#define FLINE __FILE__ ":" TOSTRING(__LINE__) + +static int next_token(context_t* ctx, int dotisspecial); + +/* + Error reporting. Call when an error is detected. Always return -1. +*/ +static int e_outofmemory(context_t* ctx, const char* fline) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "ERROR: out of memory (%s)", fline); + return -1; +} + + +static int e_internal(context_t* ctx, const char* fline) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "internal error (%s)", fline); + return -1; +} + +static int e_syntax(context_t* ctx, int lineno, const char* msg) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: %s", lineno, msg); + return -1; +} + +static int e_badkey(context_t* ctx, int lineno) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: bad key", lineno); + return -1; +} + +static int e_keyexists(context_t* ctx, int lineno) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: key exists", lineno); + return -1; +} + +static void* expand(void* p, int sz, int newsz) +{ + void* s = MALLOC(newsz); + if (!s) return 0; + + memcpy(s, p, sz); + FREE(p); + return s; +} + +static void** expand_ptrarr(void** p, int n) +{ + void** s = MALLOC((n+1) * sizeof(void*)); + if (!s) return 0; + + s[n] = 0; + memcpy(s, p, n * sizeof(void*)); + FREE(p); + return s; +} + + +static char* norm_lit_str(const char* src, int srclen, + int multiline, + char* errbuf, int errbufsz) +{ + char* dst = 0; /* will write to dst[] and return it */ + int max = 0; /* max size of dst[] */ + int off = 0; /* cur offset in dst[] */ + const char* sp = src; + const char* sq = src + srclen; + int ch; + + /* scan forward on src */ + for (;;) { + if (off >= max - 10) { /* have some slack for misc stuff */ + int newmax = max + 50; + char* x = expand(dst, max, newmax); + if (!x) { + xfree(dst); + snprintf(errbuf, errbufsz, "out of memory"); + return 0; + } + dst = x; + max = newmax; + } + + /* finished? */ + if (sp >= sq) break; + + ch = *sp++; + /* control characters other than tab is not allowed */ + if ((0 <= ch && ch <= 0x08) + || (0x0a <= ch && ch <= 0x1f) + || (ch == 0x7f)) { + if (! (multiline && (ch == '\r' || ch == '\n'))) { + xfree(dst); + snprintf(errbuf, errbufsz, "invalid char U+%04x", ch); + return 0; + } + } + + // a plain copy suffice + dst[off++] = ch; + } + + dst[off++] = 0; + return dst; +} + + + + +/* + * Convert src to raw unescaped utf-8 string. + * Returns NULL if error with errmsg in errbuf. + */ +static char* norm_basic_str(const char* src, int srclen, + int multiline, + char* errbuf, int errbufsz) +{ + char* dst = 0; /* will write to dst[] and return it */ + int max = 0; /* max size of dst[] */ + int off = 0; /* cur offset in dst[] */ + const char* sp = src; + const char* sq = src + srclen; + int ch; + + /* scan forward on src */ + for (;;) { + if (off >= max - 10) { /* have some slack for misc stuff */ + int newmax = max + 50; + char* x = expand(dst, max, newmax); + if (!x) { + xfree(dst); + snprintf(errbuf, errbufsz, "out of memory"); + return 0; + } + dst = x; + max = newmax; + } + + /* finished? */ + if (sp >= sq) break; + + ch = *sp++; + if (ch != '\\') { + /* these chars must be escaped: U+0000 to U+0008, U+000A to U+001F, U+007F */ + if ((0 <= ch && ch <= 0x08) + || (0x0a <= ch && ch <= 0x1f) + || (ch == 0x7f)) { + if (! (multiline && (ch == '\r' || ch == '\n'))) { + xfree(dst); + snprintf(errbuf, errbufsz, "invalid char U+%04x", ch); + return 0; + } + } + + // a plain copy suffice + dst[off++] = ch; + continue; + } + + /* ch was backslash. we expect the escape char. */ + if (sp >= sq) { + snprintf(errbuf, errbufsz, "last backslash is invalid"); + xfree(dst); + return 0; + } + + /* for multi-line, we want to kill line-ending-backslash ... */ + if (multiline) { + + // if there is only whitespace after the backslash ... + if (sp[strspn(sp, " \t\r")] == '\n') { + /* skip all the following whitespaces */ + sp += strspn(sp, " \t\r\n"); + continue; + } + } + + /* get the escaped char */ + ch = *sp++; + switch (ch) { + case 'u': case 'U': + { + int64_t ucs = 0; + int nhex = (ch == 'u' ? 4 : 8); + for (int i = 0; i < nhex; i++) { + if (sp >= sq) { + snprintf(errbuf, errbufsz, "\\%c expects %d hex chars", ch, nhex); + xfree(dst); + return 0; + } + ch = *sp++; + int v = ('0' <= ch && ch <= '9') + ? ch - '0' + : (('A' <= ch && ch <= 'F') ? ch - 'A' + 10 : -1); + if (-1 == v) { + snprintf(errbuf, errbufsz, "invalid hex chars for \\u or \\U"); + xfree(dst); + return 0; + } + ucs = ucs * 16 + v; + } + int n = toml_ucs_to_utf8(ucs, &dst[off]); + if (-1 == n) { + snprintf(errbuf, errbufsz, "illegal ucs code in \\u or \\U"); + xfree(dst); + return 0; + } + off += n; + } + continue; + + case 'b': ch = '\b'; break; + case 't': ch = '\t'; break; + case 'n': ch = '\n'; break; + case 'f': ch = '\f'; break; + case 'r': ch = '\r'; break; + case '"': ch = '"'; break; + case '\\': ch = '\\'; break; + default: + snprintf(errbuf, errbufsz, "illegal escape char \\%c", ch); + xfree(dst); + return 0; + } + + dst[off++] = ch; + } + + // Cap with NUL and return it. + dst[off++] = 0; + return dst; +} + + +/* Normalize a key. Convert all special chars to raw unescaped utf-8 chars. */ +static char* normalize_key(context_t* ctx, token_t strtok) +{ + const char* sp = strtok.ptr; + const char* sq = strtok.ptr + strtok.len; + int lineno = strtok.lineno; + char* ret; + int ch = *sp; + char ebuf[80]; + + /* handle quoted string */ + if (ch == '\'' || ch == '\"') { + /* if ''' or """, take 3 chars off front and back. Else, take 1 char off. */ + int multiline = 0; + if (sp[1] == ch && sp[2] == ch) { + sp += 3, sq -= 3; + multiline = 1; + } + else + sp++, sq--; + + if (ch == '\'') { + /* for single quote, take it verbatim. */ + if (! (ret = STRNDUP(sp, sq - sp))) { + e_outofmemory(ctx, FLINE); + return 0; + } + } else { + /* for double quote, we need to normalize */ + ret = norm_basic_str(sp, sq - sp, multiline, ebuf, sizeof(ebuf)); + if (!ret) { + e_syntax(ctx, lineno, ebuf); + return 0; + } + } + + /* newlines are not allowed in keys */ + if (strchr(ret, '\n')) { + xfree(ret); + e_badkey(ctx, lineno); + return 0; + } + return ret; + } + + /* for bare-key allow only this regex: [A-Za-z0-9_-]+ */ + const char* xp; + for (xp = sp; xp != sq; xp++) { + int k = *xp; + if (isalnum(k)) continue; + if (k == '_' || k == '-') continue; + e_badkey(ctx, lineno); + return 0; + } + + /* dup and return it */ + if (! (ret = STRNDUP(sp, sq - sp))) { + e_outofmemory(ctx, FLINE); + return 0; + } + return ret; +} + + +/* + * Look up key in tab. Return 0 if not found, or + * 'v'alue, 'a'rray or 't'able depending on the element. + */ +static int check_key(toml_table_t* tab, const char* key, + toml_keyval_t** ret_val, + toml_array_t** ret_arr, + toml_table_t** ret_tab) +{ + int i; + void* dummy; + + if (!ret_tab) ret_tab = (toml_table_t**) &dummy; + if (!ret_arr) ret_arr = (toml_array_t**) &dummy; + if (!ret_val) ret_val = (toml_keyval_t**) &dummy; + + *ret_tab = 0; *ret_arr = 0; *ret_val = 0; + + for (i = 0; i < tab->nkval; i++) { + if (0 == strcmp(key, tab->kval[i]->key)) { + *ret_val = tab->kval[i]; + return 'v'; + } + } + for (i = 0; i < tab->narr; i++) { + if (0 == strcmp(key, tab->arr[i]->key)) { + *ret_arr = tab->arr[i]; + return 'a'; + } + } + for (i = 0; i < tab->ntab; i++) { + if (0 == strcmp(key, tab->tab[i]->key)) { + *ret_tab = tab->tab[i]; + return 't'; + } + } + return 0; +} + + +static int key_kind(toml_table_t* tab, const char* key) +{ + return check_key(tab, key, 0, 0, 0); +} + +/* Create a keyval in the table. + */ +static toml_keyval_t* create_keyval_in_table(context_t* ctx, toml_table_t* tab, token_t keytok) +{ + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char* newkey = normalize_key(ctx, keytok); + if (!newkey) return 0; + + /* if key exists: error out. */ + toml_keyval_t* dest = 0; + if (key_kind(tab, newkey)) { + xfree(newkey); + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* make a new entry */ + int n = tab->nkval; + toml_keyval_t** base; + if (0 == (base = (toml_keyval_t**) expand_ptrarr((void**)tab->kval, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->kval = base; + + if (0 == (base[n] = (toml_keyval_t*) CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + dest = tab->kval[tab->nkval++]; + + /* save the key in the new value struct */ + dest->key = newkey; + return dest; +} + + +/* Create a table in the table. + */ +static toml_table_t* create_keytable_in_table(context_t* ctx, toml_table_t* tab, token_t keytok) +{ + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char* newkey = normalize_key(ctx, keytok); + if (!newkey) return 0; + + /* if key exists: error out */ + toml_table_t* dest = 0; + if (check_key(tab, newkey, 0, 0, &dest)) { + xfree(newkey); /* don't need this anymore */ + + /* special case: if table exists, but was created implicitly ... */ + if (dest && dest->implicit) { + /* we make it explicit now, and simply return it. */ + dest->implicit = false; + return dest; + } + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* create a new table entry */ + int n = tab->ntab; + toml_table_t** base; + if (0 == (base = (toml_table_t**) expand_ptrarr((void**)tab->tab, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->tab = base; + + if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + dest = tab->tab[tab->ntab++]; + + /* save the key in the new table struct */ + dest->key = newkey; + return dest; +} + + +/* Create an array in the table. + */ +static toml_array_t* create_keyarray_in_table(context_t* ctx, + toml_table_t* tab, + token_t keytok, + char kind) +{ + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char* newkey = normalize_key(ctx, keytok); + if (!newkey) return 0; + + /* if key exists: error out */ + if (key_kind(tab, newkey)) { + xfree(newkey); /* don't need this anymore */ + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* make a new array entry */ + int n = tab->narr; + toml_array_t** base; + if (0 == (base = (toml_array_t**) expand_ptrarr((void**)tab->arr, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->arr = base; + + if (0 == (base[n] = (toml_array_t*) CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + toml_array_t* dest = tab->arr[tab->narr++]; + + /* save the key in the new array struct */ + dest->key = newkey; + dest->kind = kind; + return dest; +} + +/* Create an array in an array + */ +static toml_array_t* create_array_in_array(context_t* ctx, + toml_array_t* parent) +{ + const int n = parent->nelem; + toml_array_t** base; + if (0 == (base = (toml_array_t**) expand_ptrarr((void**)parent->u.arr, n))) { + e_outofmemory(ctx, FLINE); + return 0; + } + parent->u.arr = base; + parent->nelem++; + + if (0 == (base[n] = (toml_array_t*) CALLOC(1, sizeof(*base[n])))) { + e_outofmemory(ctx, FLINE); + return 0; + } + + return parent->u.arr[n]; +} + +/* Create a table in an array + */ +static toml_table_t* create_table_in_array(context_t* ctx, + toml_array_t* parent) +{ + int n = parent->nelem; + toml_table_t** base; + if (0 == (base = (toml_table_t**) expand_ptrarr((void**)parent->u.tab, n))) { + e_outofmemory(ctx, FLINE); + return 0; + } + parent->u.tab = base; + + if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) { + e_outofmemory(ctx, FLINE); + return 0; + } + + return parent->u.tab[parent->nelem++]; +} + + +static int skip_newlines(context_t* ctx, int isdotspecial) +{ + while (ctx->tok.tok == NEWLINE) { + if (next_token(ctx, isdotspecial)) return -1; + if (ctx->tok.eof) break; + } + return 0; +} + + +static int parse_keyval(context_t* ctx, toml_table_t* tab); + +static inline int eat_token(context_t* ctx, tokentype_t typ, int isdotspecial, const char* fline) +{ + if (ctx->tok.tok != typ) + return e_internal(ctx, fline); + + if (next_token(ctx, isdotspecial)) + return -1; + + return 0; +} + + + +/* We are at '{ ... }'. + * Parse the table. + */ +static int parse_table(context_t* ctx, toml_table_t* tab) +{ + if (eat_token(ctx, LBRACE, 1, FLINE)) + return -1; + + for (;;) { + if (ctx->tok.tok == NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, "newline not allowed in inline table"); + + /* until } */ + if (ctx->tok.tok == RBRACE) + break; + + if (ctx->tok.tok != STRING) + return e_syntax(ctx, ctx->tok.lineno, "expect a string"); + + if (parse_keyval(ctx, tab)) + return -1; + + if (ctx->tok.tok == NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, "newline not allowed in inline table"); + + /* on comma, continue to scan for next keyval */ + if (ctx->tok.tok == COMMA) { + if (eat_token(ctx, COMMA, 1, FLINE)) + return -1; + continue; + } + break; + } + + if (eat_token(ctx, RBRACE, 1, FLINE)) + return -1; + return 0; +} + +static int valtype(const char* val) +{ + toml_timestamp_t ts; + if (*val == '\'' || *val == '"') return 's'; + if (0 == toml_rtob(val, 0)) return 'b'; + if (0 == toml_rtoi(val, 0)) return 'i'; + if (0 == toml_rtod(val, 0)) return 'd'; + if (0 == toml_rtots(val, &ts)) { + if (ts.year && ts.hour) return 'T'; /* timestamp */ + if (ts.year) return 'D'; /* date */ + return 't'; /* time */ + } + return 'u'; /* unknown */ +} + + +/* We are at '[...]' */ +static int parse_array(context_t* ctx, toml_array_t* arr) +{ + if (eat_token(ctx, LBRACKET, 0, FLINE)) return -1; + + for (;;) { + if (skip_newlines(ctx, 0)) return -1; + + /* until ] */ + if (ctx->tok.tok == RBRACKET) break; + + switch (ctx->tok.tok) { + case STRING: + { + char* val = ctx->tok.ptr; + int vlen = ctx->tok.len; + + /* set array kind if this will be the first entry */ + if (arr->kind == 0) arr->kind = 'v'; + /* check array kind */ + if (arr->kind != 'v') + return e_syntax(ctx, ctx->tok.lineno, "a string array can only contain strings"); + + /* make a new value in array */ + char** tmp = (char**) expand_ptrarr((void**)arr->u.val, arr->nelem); + if (!tmp) + return e_outofmemory(ctx, FLINE); + + arr->u.val = tmp; + if (! (val = STRNDUP(val, vlen))) + return e_outofmemory(ctx, FLINE); + + arr->u.val[arr->nelem++] = val; + + /* set array type if this is the first entry, or check that the types matched. */ + if (arr->nelem == 1) + arr->type = valtype(arr->u.val[0]); + else if (arr->type != valtype(val)) { + return e_syntax(ctx, ctx->tok.lineno, + "array type mismatch while processing array of values"); + } + + if (eat_token(ctx, STRING, 0, FLINE)) return -1; + break; + } + + case LBRACKET: + { /* [ [array], [array] ... ] */ + /* set the array kind if this will be the first entry */ + if (arr->kind == 0) arr->kind = 'a'; + /* check array kind */ + if (arr->kind != 'a') { + return e_syntax(ctx, ctx->tok.lineno, + "array type mismatch while processing array of arrays"); + } + toml_array_t* subarr = create_array_in_array(ctx, arr); + if (!subarr) return -1; + if (parse_array(ctx, subarr)) return -1; + break; + } + + case LBRACE: + { /* [ {table}, {table} ... ] */ + /* set the array kind if this will be the first entry */ + if (arr->kind == 0) arr->kind = 't'; + /* check array kind */ + if (arr->kind != 't') { + return e_syntax(ctx, ctx->tok.lineno, + "array type mismatch while processing array of tables"); + } + toml_table_t* subtab = create_table_in_array(ctx, arr); + if (!subtab) return -1; + if (parse_table(ctx, subtab)) return -1; + break; + } + + default: + return e_syntax(ctx, ctx->tok.lineno, "syntax error"); + } + + if (skip_newlines(ctx, 0)) return -1; + + /* on comma, continue to scan for next element */ + if (ctx->tok.tok == COMMA) { + if (eat_token(ctx, COMMA, 0, FLINE)) return -1; + continue; + } + break; + } + + if (eat_token(ctx, RBRACKET, 1, FLINE)) return -1; + return 0; +} + + +/* handle lines like these: + key = "value" + key = [ array ] + key = { table } +*/ +static int parse_keyval(context_t* ctx, toml_table_t* tab) +{ + token_t key = ctx->tok; + if (eat_token(ctx, STRING, 1, FLINE)) return -1; + + if (ctx->tok.tok == DOT) { + /* handle inline dotted key. + e.g. + physical.color = "orange" + physical.shape = "round" + */ + toml_table_t* subtab = 0; + { + char* subtabstr = normalize_key(ctx, key); + subtab = toml_table_in(tab, subtabstr); + xfree(subtabstr); + } + if (!subtab) { + subtab = create_keytable_in_table(ctx, tab, key); + if (!subtab) return -1; + } + if (next_token(ctx, 1)) return -1; + if (parse_keyval(ctx, subtab)) return -1; + return 0; + } + + if (ctx->tok.tok != EQUAL) { + return e_syntax(ctx, ctx->tok.lineno, "missing ="); + } + + if (next_token(ctx, 0)) return -1; + + switch (ctx->tok.tok) { + case STRING: + { /* key = "value" */ + toml_keyval_t* keyval = create_keyval_in_table(ctx, tab, key); + if (!keyval) return -1; + token_t val = ctx->tok; + + assert(keyval->val == 0); + if (! (keyval->val = STRNDUP(val.ptr, val.len))) + return e_outofmemory(ctx, FLINE); + + if (next_token(ctx, 1)) return -1; + + return 0; + } + + case LBRACKET: + { /* key = [ array ] */ + toml_array_t* arr = create_keyarray_in_table(ctx, tab, key, 0); + if (!arr) return -1; + if (parse_array(ctx, arr)) return -1; + return 0; + } + + case LBRACE: + { /* key = { table } */ + toml_table_t* nxttab = create_keytable_in_table(ctx, tab, key); + if (!nxttab) return -1; + if (parse_table(ctx, nxttab)) return -1; + return 0; + } + + default: + return e_syntax(ctx, ctx->tok.lineno, "syntax error"); + } + return 0; +} + + +typedef struct tabpath_t tabpath_t; +struct tabpath_t { + int cnt; + token_t key[10]; +}; + +/* at [x.y.z] or [[x.y.z]] + * Scan forward and fill tabpath until it enters ] or ]] + * There will be at least one entry on return. + */ +static int fill_tabpath(context_t* ctx) +{ + int lineno = ctx->tok.lineno; + int i; + + /* clear tpath */ + for (i = 0; i < ctx->tpath.top; i++) { + char** p = &ctx->tpath.key[i]; + xfree(*p); + *p = 0; + } + ctx->tpath.top = 0; + + for (;;) { + if (ctx->tpath.top >= 10) + return e_syntax(ctx, lineno, "table path is too deep; max allowed is 10."); + + if (ctx->tok.tok != STRING) + return e_syntax(ctx, lineno, "invalid or missing key"); + + char* key = normalize_key(ctx, ctx->tok); + if (!key) return -1; + ctx->tpath.tok[ctx->tpath.top] = ctx->tok; + ctx->tpath.key[ctx->tpath.top] = key; + ctx->tpath.top++; + + if (next_token(ctx, 1)) return -1; + + if (ctx->tok.tok == RBRACKET) break; + + if (ctx->tok.tok != DOT) + return e_syntax(ctx, lineno, "invalid key"); + + if (next_token(ctx, 1)) return -1; + } + + if (ctx->tpath.top <= 0) + return e_syntax(ctx, lineno, "empty table selector"); + + return 0; +} + + +/* Walk tabpath from the root, and create new tables on the way. + * Sets ctx->curtab to the final table. + */ +static int walk_tabpath(context_t* ctx) +{ + /* start from root */ + toml_table_t* curtab = ctx->root; + + for (int i = 0; i < ctx->tpath.top; i++) { + const char* key = ctx->tpath.key[i]; + + toml_keyval_t* nextval = 0; + toml_array_t* nextarr = 0; + toml_table_t* nexttab = 0; + switch (check_key(curtab, key, &nextval, &nextarr, &nexttab)) { + case 't': + /* found a table. nexttab is where we will go next. */ + break; + + case 'a': + /* found an array. nexttab is the last table in the array. */ + if (nextarr->kind != 't') + return e_internal(ctx, FLINE); + + if (nextarr->nelem == 0) + return e_internal(ctx, FLINE); + + nexttab = nextarr->u.tab[nextarr->nelem-1]; + break; + + case 'v': + return e_keyexists(ctx, ctx->tpath.tok[i].lineno); + + default: + { /* Not found. Let's create an implicit table. */ + int n = curtab->ntab; + toml_table_t** base = (toml_table_t**) expand_ptrarr((void**)curtab->tab, n); + if (0 == base) + return e_outofmemory(ctx, FLINE); + + curtab->tab = base; + + if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) + return e_outofmemory(ctx, FLINE); + + if (0 == (base[n]->key = STRDUP(key))) + return e_outofmemory(ctx, FLINE); + + nexttab = curtab->tab[curtab->ntab++]; + + /* tabs created by walk_tabpath are considered implicit */ + nexttab->implicit = true; + } + break; + } + + /* switch to next tab */ + curtab = nexttab; + } + + /* save it */ + ctx->curtab = curtab; + + return 0; +} + + +/* handle lines like [x.y.z] or [[x.y.z]] */ +static int parse_select(context_t* ctx) +{ + assert(ctx->tok.tok == LBRACKET); + + /* true if [[ */ + int llb = (ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == '['); + /* need to detect '[[' on our own because next_token() will skip whitespace, + and '[ [' would be taken as '[[', which is wrong. */ + + /* eat [ or [[ */ + if (eat_token(ctx, LBRACKET, 1, FLINE)) return -1; + if (llb) { + assert(ctx->tok.tok == LBRACKET); + if (eat_token(ctx, LBRACKET, 1, FLINE)) return -1; + } + + if (fill_tabpath(ctx)) return -1; + + /* For [x.y.z] or [[x.y.z]], remove z from tpath. + */ + token_t z = ctx->tpath.tok[ctx->tpath.top-1]; + xfree(ctx->tpath.key[ctx->tpath.top-1]); + ctx->tpath.top--; + + /* set up ctx->curtab */ + if (walk_tabpath(ctx)) return -1; + + if (! llb) { + /* [x.y.z] -> create z = {} in x.y */ + toml_table_t* curtab = create_keytable_in_table(ctx, ctx->curtab, z); + if (!curtab) return -1; + ctx->curtab = curtab; + } else { + /* [[x.y.z]] -> create z = [] in x.y */ + toml_array_t* arr = 0; + { + char* zstr = normalize_key(ctx, z); + if (!zstr) return -1; + arr = toml_array_in(ctx->curtab, zstr); + xfree(zstr); + } + if (!arr) { + arr = create_keyarray_in_table(ctx, ctx->curtab, z, 't'); + if (!arr) return -1; + } + if (arr->kind != 't') + return e_syntax(ctx, z.lineno, "array mismatch"); + + /* add to z[] */ + toml_table_t* dest; + { + int n = arr->nelem; + toml_table_t** base = (toml_table_t**) expand_ptrarr((void**)arr->u.tab, n); + if (0 == base) + return e_outofmemory(ctx, FLINE); + + arr->u.tab = base; + + if (0 == (base[n] = CALLOC(1, sizeof(*base[n])))) + return e_outofmemory(ctx, FLINE); + + if (0 == (base[n]->key = STRDUP("__anon__"))) + return e_outofmemory(ctx, FLINE); + + dest = arr->u.tab[arr->nelem++]; + } + + ctx->curtab = dest; + } + + if (ctx->tok.tok != RBRACKET) { + return e_syntax(ctx, ctx->tok.lineno, "expects ]"); + } + if (llb) { + if (! (ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == ']')) { + return e_syntax(ctx, ctx->tok.lineno, "expects ]]"); + } + if (eat_token(ctx, RBRACKET, 1, FLINE)) return -1; + } + + if (eat_token(ctx, RBRACKET, 1, FLINE)) + return -1; + + if (ctx->tok.tok != NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, "extra chars after ] or ]]"); + + return 0; +} + + + + +toml_table_t* toml_parse(char* conf, + char* errbuf, + int errbufsz) +{ + context_t ctx; + + // clear errbuf + if (errbufsz <= 0) errbufsz = 0; + if (errbufsz > 0) errbuf[0] = 0; + + // init context + memset(&ctx, 0, sizeof(ctx)); + ctx.start = conf; + ctx.stop = ctx.start + strlen(conf); + ctx.errbuf = errbuf; + ctx.errbufsz = errbufsz; + + // start with an artificial newline of length 0 + ctx.tok.tok = NEWLINE; + ctx.tok.lineno = 1; + ctx.tok.ptr = conf; + ctx.tok.len = 0; + + // make a root table + if (0 == (ctx.root = CALLOC(1, sizeof(*ctx.root)))) { + e_outofmemory(&ctx, FLINE); + // Do not goto fail, root table not set up yet + return 0; + } + + // set root as default table + ctx.curtab = ctx.root; + + /* Scan forward until EOF */ + for (token_t tok = ctx.tok; ! tok.eof ; tok = ctx.tok) { + switch (tok.tok) { + + case NEWLINE: + if (next_token(&ctx, 1)) goto fail; + break; + + case STRING: + if (parse_keyval(&ctx, ctx.curtab)) goto fail; + + if (ctx.tok.tok != NEWLINE) { + e_syntax(&ctx, ctx.tok.lineno, "extra chars after value"); + goto fail; + } + + if (eat_token(&ctx, NEWLINE, 1, FLINE)) goto fail; + break; + + case LBRACKET: /* [ x.y.z ] or [[ x.y.z ]] */ + if (parse_select(&ctx)) goto fail; + break; + + default: + e_syntax(&ctx, tok.lineno, "syntax error"); + goto fail; + } + } + + /* success */ + for (int i = 0; i < ctx.tpath.top; i++) xfree(ctx.tpath.key[i]); + return ctx.root; + +fail: + // Something bad has happened. Free resources and return error. + for (int i = 0; i < ctx.tpath.top; i++) xfree(ctx.tpath.key[i]); + toml_free(ctx.root); + return 0; +} + + +toml_table_t* toml_parse_file(FILE* fp, + char* errbuf, + int errbufsz) +{ + int bufsz = 0; + char* buf = 0; + int off = 0; + + /* read from fp into buf */ + while (! feof(fp)) { + + if (off == bufsz) { + int xsz = bufsz + 1000; + char* x = expand(buf, bufsz, xsz); + if (!x) { + snprintf(errbuf, errbufsz, "out of memory"); + xfree(buf); + return 0; + } + buf = x; + bufsz = xsz; + } + + errno = 0; + int n = fread(buf + off, 1, bufsz - off, fp); + if (ferror(fp)) { + snprintf(errbuf, errbufsz, "%s", + errno ? strerror(errno) : "Error reading file"); + xfree(buf); + return 0; + } + off += n; + } + + /* tag on a NUL to cap the string */ + if (off == bufsz) { + int xsz = bufsz + 1; + char* x = expand(buf, bufsz, xsz); + if (!x) { + snprintf(errbuf, errbufsz, "out of memory"); + xfree(buf); + return 0; + } + buf = x; + bufsz = xsz; + } + buf[off] = 0; + + /* parse it, cleanup and finish */ + toml_table_t* ret = toml_parse(buf, errbuf, errbufsz); + xfree(buf); + return ret; +} + + +static void xfree_kval(toml_keyval_t* p) +{ + if (!p) return; + xfree(p->key); + xfree(p->val); + xfree(p); +} + +static void xfree_tab(toml_table_t* p); + +static void xfree_arr(toml_array_t* p) +{ + if (!p) return; + + xfree(p->key); + switch (p->kind) { + case 'v': + for (int i = 0; i < p->nelem; i++) xfree(p->u.val[i]); + xfree(p->u.val); + break; + + case 'a': + for (int i = 0; i < p->nelem; i++) xfree_arr(p->u.arr[i]); + xfree(p->u.arr); + break; + + case 't': + for (int i = 0; i < p->nelem; i++) xfree_tab(p->u.tab[i]); + xfree(p->u.tab); + break; + } + + xfree(p); +} + + +static void xfree_tab(toml_table_t* p) +{ + int i; + + if (!p) return; + + xfree(p->key); + + for (i = 0; i < p->nkval; i++) xfree_kval(p->kval[i]); + xfree(p->kval); + + for (i = 0; i < p->narr; i++) xfree_arr(p->arr[i]); + xfree(p->arr); + + for (i = 0; i < p->ntab; i++) xfree_tab(p->tab[i]); + xfree(p->tab); + + xfree(p); +} + + +void toml_free(toml_table_t* tab) +{ + xfree_tab(tab); +} + + +static void set_token(context_t* ctx, tokentype_t tok, int lineno, char* ptr, int len) +{ + token_t t; + t.tok = tok; + t.lineno = lineno; + t.ptr = ptr; + t.len = len; + t.eof = 0; + ctx->tok = t; +} + +static void set_eof(context_t* ctx, int lineno) +{ + set_token(ctx, NEWLINE, lineno, ctx->stop, 0); + ctx->tok.eof = 1; +} + + +/* Scan p for n digits compositing entirely of [0-9] */ +static int scan_digits(const char* p, int n) +{ + int ret = 0; + for ( ; n > 0 && isdigit(*p); n--, p++) { + ret = 10 * ret + (*p - '0'); + } + return n ? -1 : ret; +} + +static int scan_date(const char* p, int* YY, int* MM, int* DD) +{ + int year, month, day; + year = scan_digits(p, 4); + month = (year >= 0 && p[4] == '-') ? scan_digits(p+5, 2) : -1; + day = (month >= 0 && p[7] == '-') ? scan_digits(p+8, 2) : -1; + if (YY) *YY = year; + if (MM) *MM = month; + if (DD) *DD = day; + return (year >= 0 && month >= 0 && day >= 0) ? 0 : -1; +} + +static int scan_time(const char* p, int* hh, int* mm, int* ss) +{ + int hour, minute, second; + hour = scan_digits(p, 2); + minute = (hour >= 0 && p[2] == ':') ? scan_digits(p+3, 2) : -1; + second = (minute >= 0 && p[5] == ':') ? scan_digits(p+6, 2) : -1; + if (hh) *hh = hour; + if (mm) *mm = minute; + if (ss) *ss = second; + return (hour >= 0 && minute >= 0 && second >= 0) ? 0 : -1; +} + + +static int scan_string(context_t* ctx, char* p, int lineno, int dotisspecial) +{ + char* orig = p; + if (0 == strncmp(p, "'''", 3)) { + p = strstr(p + 3, "'''"); + if (0 == p) { + return e_syntax(ctx, lineno, "unterminated triple-s-quote"); + } + + set_token(ctx, STRING, lineno, orig, p + 3 - orig); + return 0; + } + + if (0 == strncmp(p, "\"\"\"", 3)) { + int hexreq = 0; /* #hex required */ + int escape = 0; + int qcnt = 0; /* count quote */ + for (p += 3; *p && qcnt < 3; p++) { + if (escape) { + escape = 0; + if (strchr("btnfr\"\\", *p)) continue; + if (*p == 'u') { hexreq = 4; continue; } + if (*p == 'U') { hexreq = 8; continue; } + if (p[strspn(p, " \t\r")] == '\n') continue; /* allow for line ending backslash */ + return e_syntax(ctx, lineno, "bad escape char"); + } + if (hexreq) { + hexreq--; + if (strchr("0123456789ABCDEF", *p)) continue; + return e_syntax(ctx, lineno, "expect hex char"); + } + if (*p == '\\') { escape = 1; continue; } + qcnt = (*p == '"') ? qcnt + 1 : 0; + } + if (qcnt != 3) { + return e_syntax(ctx, lineno, "unterminated triple-quote"); + } + + set_token(ctx, STRING, lineno, orig, p - orig); + return 0; + } + + if ('\'' == *p) { + for (p++; *p && *p != '\n' && *p != '\''; p++); + if (*p != '\'') { + return e_syntax(ctx, lineno, "unterminated s-quote"); + } + + set_token(ctx, STRING, lineno, orig, p + 1 - orig); + return 0; + } + + if ('\"' == *p) { + int hexreq = 0; /* #hex required */ + int escape = 0; + for (p++; *p; p++) { + if (escape) { + escape = 0; + if (strchr("btnfr\"\\", *p)) continue; + if (*p == 'u') { hexreq = 4; continue; } + if (*p == 'U') { hexreq = 8; continue; } + return e_syntax(ctx, lineno, "bad escape char"); + } + if (hexreq) { + hexreq--; + if (strchr("0123456789ABCDEF", *p)) continue; + return e_syntax(ctx, lineno, "expect hex char"); + } + if (*p == '\\') { escape = 1; continue; } + if (*p == '\n') break; + if (*p == '"') break; + } + if (*p != '"') { + return e_syntax(ctx, lineno, "unterminated quote"); + } + + set_token(ctx, STRING, lineno, orig, p + 1 - orig); + return 0; + } + + /* check for timestamp without quotes */ + if (0 == scan_date(p, 0, 0, 0) || 0 == scan_time(p, 0, 0, 0)) { + // forward thru the timestamp + for ( ; strchr("0123456789.:+-T Z", toupper(*p)); p++); + // squeeze out any spaces at end of string + for ( ; p[-1] == ' '; p--); + // tokenize + set_token(ctx, STRING, lineno, orig, p - orig); + return 0; + } + + /* literals */ + for ( ; *p && *p != '\n'; p++) { + int ch = *p; + if (ch == '.' && dotisspecial) break; + if ('A' <= ch && ch <= 'Z') continue; + if ('a' <= ch && ch <= 'z') continue; + if (strchr("0123456789+-_.", ch)) continue; + break; + } + + set_token(ctx, STRING, lineno, orig, p - orig); + return 0; +} + + +static int next_token(context_t* ctx, int dotisspecial) +{ + int lineno = ctx->tok.lineno; + char* p = ctx->tok.ptr; + int i; + + /* eat this tok */ + for (i = 0; i < ctx->tok.len; i++) { + if (*p++ == '\n') + lineno++; + } + + /* make next tok */ + while (p < ctx->stop) { + /* skip comment. stop just before the \n. */ + if (*p == '#') { + for (p++; p < ctx->stop && *p != '\n'; p++); + continue; + } + + if (dotisspecial && *p == '.') { + set_token(ctx, DOT, lineno, p, 1); + return 0; + } + + switch (*p) { + case ',': set_token(ctx, COMMA, lineno, p, 1); return 0; + case '=': set_token(ctx, EQUAL, lineno, p, 1); return 0; + case '{': set_token(ctx, LBRACE, lineno, p, 1); return 0; + case '}': set_token(ctx, RBRACE, lineno, p, 1); return 0; + case '[': set_token(ctx, LBRACKET, lineno, p, 1); return 0; + case ']': set_token(ctx, RBRACKET, lineno, p, 1); return 0; + case '\n': set_token(ctx, NEWLINE, lineno, p, 1); return 0; + case '\r': case ' ': case '\t': + /* ignore white spaces */ + p++; + continue; + } + + return scan_string(ctx, p, lineno, dotisspecial); + } + + set_eof(ctx, lineno); + return 0; +} + + +const char* toml_key_in(const toml_table_t* tab, int keyidx) +{ + if (keyidx < tab->nkval) return tab->kval[keyidx]->key; + + keyidx -= tab->nkval; + if (keyidx < tab->narr) return tab->arr[keyidx]->key; + + keyidx -= tab->narr; + if (keyidx < tab->ntab) return tab->tab[keyidx]->key; + + return 0; +} + +toml_raw_t toml_raw_in(const toml_table_t* tab, const char* key) +{ + int i; + for (i = 0; i < tab->nkval; i++) { + if (0 == strcmp(key, tab->kval[i]->key)) + return tab->kval[i]->val; + } + return 0; +} + +toml_array_t* toml_array_in(const toml_table_t* tab, const char* key) +{ + int i; + for (i = 0; i < tab->narr; i++) { + if (0 == strcmp(key, tab->arr[i]->key)) + return tab->arr[i]; + } + return 0; +} + + +toml_table_t* toml_table_in(const toml_table_t* tab, const char* key) +{ + int i; + for (i = 0; i < tab->ntab; i++) { + if (0 == strcmp(key, tab->tab[i]->key)) + return tab->tab[i]; + } + return 0; +} + +toml_raw_t toml_raw_at(const toml_array_t* arr, int idx) +{ + if (arr->kind != 'v') + return 0; + if (! (0 <= idx && idx < arr->nelem)) + return 0; + return arr->u.val[idx]; +} + +char toml_array_kind(const toml_array_t* arr) +{ + return arr->kind; +} + +char toml_array_type(const toml_array_t* arr) +{ + if (arr->kind != 'v') + return 0; + + if (arr->nelem == 0) + return 0; + + return arr->type; +} + + +int toml_array_nelem(const toml_array_t* arr) +{ + return arr->nelem; +} + +const char* toml_array_key(const toml_array_t* arr) +{ + return arr ? arr->key : (const char*) NULL; +} + +int toml_table_nkval(const toml_table_t* tab) +{ + return tab->nkval; +} + +int toml_table_narr(const toml_table_t* tab) +{ + return tab->narr; +} + +int toml_table_ntab(const toml_table_t* tab) +{ + return tab->ntab; +} + +const char* toml_table_key(const toml_table_t* tab) +{ + return tab ? tab->key : (const char*) NULL; +} + +toml_array_t* toml_array_at(const toml_array_t* arr, int idx) +{ + if (arr->kind != 'a') + return 0; + if (! (0 <= idx && idx < arr->nelem)) + return 0; + return arr->u.arr[idx]; +} + +toml_table_t* toml_table_at(const toml_array_t* arr, int idx) +{ + if (arr->kind != 't') + return 0; + if (! (0 <= idx && idx < arr->nelem)) + return 0; + return arr->u.tab[idx]; +} + + +int toml_rtots(toml_raw_t src_, toml_timestamp_t* ret) +{ + if (! src_) return -1; + + const char* p = src_; + int must_parse_time = 0; + + memset(ret, 0, sizeof(*ret)); + + int* year = &ret->__buffer.year; + int* month = &ret->__buffer.month; + int* day = &ret->__buffer.day; + int* hour = &ret->__buffer.hour; + int* minute = &ret->__buffer.minute; + int* second = &ret->__buffer.second; + int* millisec = &ret->__buffer.millisec; + + /* parse date YYYY-MM-DD */ + if (0 == scan_date(p, year, month, day)) { + ret->year = year; + ret->month = month; + ret->day = day; + + p += 10; + if (*p) { + // parse the T or space separator + if (*p != 'T' && *p != ' ') return -1; + must_parse_time = 1; + p++; + } + } + + /* parse time HH:MM:SS */ + if (0 == scan_time(p, hour, minute, second)) { + ret->hour = hour; + ret->minute = minute; + ret->second = second; + + /* optionally, parse millisec */ + p += 8; + if (*p == '.') { + char* qq; + p++; + errno = 0; + *millisec = strtol(p, &qq, 0); + if (errno) { + return -1; + } + while (*millisec > 999) { + *millisec /= 10; + } + + ret->millisec = millisec; + p = qq; + } + + if (*p) { + /* parse and copy Z */ + char* z = ret->__buffer.z; + ret->z = z; + if (*p == 'Z' || *p == 'z') { + *z++ = 'Z'; p++; + *z = 0; + + } else if (*p == '+' || *p == '-') { + *z++ = *p++; + + if (! (isdigit(p[0]) && isdigit(p[1]))) return -1; + *z++ = *p++; + *z++ = *p++; + + if (*p == ':') { + *z++ = *p++; + + if (! (isdigit(p[0]) && isdigit(p[1]))) return -1; + *z++ = *p++; + *z++ = *p++; + } + + *z = 0; + } + } + } + if (*p != 0) + return -1; + + if (must_parse_time && !ret->hour) + return -1; + + return 0; +} + + +/* Raw to boolean */ +int toml_rtob(toml_raw_t src, int* ret_) +{ + if (!src) return -1; + int dummy; + int* ret = ret_ ? ret_ : &dummy; + + if (0 == strcmp(src, "true")) { + *ret = 1; + return 0; + } + if (0 == strcmp(src, "false")) { + *ret = 0; + return 0; + } + return -1; +} + + +/* Raw to integer */ +int toml_rtoi(toml_raw_t src, int64_t* ret_) +{ + if (!src) return -1; + + char buf[100]; + char* p = buf; + char* q = p + sizeof(buf); + const char* s = src; + int base = 0; + int64_t dummy; + int64_t* ret = ret_ ? ret_ : &dummy; + + + /* allow +/- */ + if (s[0] == '+' || s[0] == '-') + *p++ = *s++; + + /* disallow +_100 */ + if (s[0] == '_') + return -1; + + /* if 0 ... */ + if ('0' == s[0]) { + switch (s[1]) { + case 'x': base = 16; s += 2; break; + case 'o': base = 8; s += 2; break; + case 'b': base = 2; s += 2; break; + case '\0': return *ret = 0, 0; + default: + /* ensure no other digits after it */ + if (s[1]) return -1; + } + } + + /* just strip underscores and pass to strtoll */ + while (*s && p < q) { + int ch = *s++; + switch (ch) { + case '_': + // disallow '__' + if (s[0] == '_') return -1; + continue; /* skip _ */ + default: + break; + } + *p++ = ch; + } + if (*s || p == q) return -1; + + /* last char cannot be '_' */ + if (s[-1] == '_') return -1; + + /* cap with NUL */ + *p = 0; + + /* Run strtoll on buf to get the integer */ + char* endp; + errno = 0; + *ret = strtoll(buf, &endp, base); + return (errno || *endp) ? -1 : 0; +} + + +int toml_rtod_ex(toml_raw_t src, double* ret_, char* buf, int buflen) +{ + if (!src) return -1; + + char* p = buf; + char* q = p + buflen; + const char* s = src; + double dummy; + double* ret = ret_ ? ret_ : &dummy; + + + /* allow +/- */ + if (s[0] == '+' || s[0] == '-') + *p++ = *s++; + + /* disallow +_1.00 */ + if (s[0] == '_') + return -1; + + /* disallow +.99 */ + if (s[0] == '.') + return -1; + + /* zero must be followed by . or 'e', or NUL */ + if (s[0] == '0' && s[1] && !strchr("eE.", s[1])) + return -1; + + /* just strip underscores and pass to strtod */ + while (*s && p < q) { + int ch = *s++; + switch (ch) { + case '.': + if (s[-2] == '_') return -1; + if (s[0] == '_') return -1; + break; + case '_': + // disallow '__' + if (s[0] == '_') return -1; + continue; /* skip _ */ + default: + break; + } + *p++ = ch; + } + if (*s || p == q) return -1; /* reached end of string or buffer is full? */ + + /* last char cannot be '_' */ + if (s[-1] == '_') return -1; + + if (p != buf && p[-1] == '.') + return -1; /* no trailing zero */ + + /* cap with NUL */ + *p = 0; + + /* Run strtod on buf to get the value */ + char* endp; + errno = 0; + *ret = strtod(buf, &endp); + return (errno || *endp) ? -1 : 0; +} + +int toml_rtod(toml_raw_t src, double* ret_) +{ + char buf[100]; + return toml_rtod_ex(src, ret_, buf, sizeof(buf)); +} + + + + +int toml_rtos(toml_raw_t src, char** ret) +{ + int multiline = 0; + const char* sp; + const char* sq; + + *ret = 0; + if (!src) return -1; + + int qchar = src[0]; + int srclen = strlen(src); + if (! (qchar == '\'' || qchar == '"')) { + return -1; + } + + // triple quotes? + if (qchar == src[1] && qchar == src[2]) { + multiline = 1; + sp = src + 3; + sq = src + srclen - 3; + /* last 3 chars in src must be qchar */ + if (! (sp <= sq && sq[0] == qchar && sq[1] == qchar && sq[2] == qchar)) + return -1; + + /* skip new line immediate after qchar */ + if (sp[0] == '\n') + sp++; + else if (sp[0] == '\r' && sp[1] == '\n') + sp += 2; + + } else { + sp = src + 1; + sq = src + srclen - 1; + /* last char in src must be qchar */ + if (! (sp <= sq && *sq == qchar)) + return -1; + } + + if (qchar == '\'') { + *ret = norm_lit_str(sp, sq - sp, + multiline, + 0, 0); + } else { + *ret = norm_basic_str(sp, sq - sp, + multiline, + 0, 0); + } + + return *ret ? 0 : -1; +} + + +toml_datum_t toml_string_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtos(toml_raw_at(arr, idx), &ret.u.s)); + return ret; +} + +toml_datum_t toml_bool_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtob(toml_raw_at(arr, idx), &ret.u.b)); + return ret; +} + +toml_datum_t toml_int_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtoi(toml_raw_at(arr, idx), &ret.u.i)); + return ret; +} + +toml_datum_t toml_double_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtod(toml_raw_at(arr, idx), &ret.u.d)); + return ret; +} + +toml_datum_t toml_timestamp_at(const toml_array_t* arr, int idx) +{ + toml_timestamp_t ts; + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtots(toml_raw_at(arr, idx), &ts)); + if (ret.ok) { + ret.ok = !!(ret.u.ts = malloc(sizeof(*ret.u.ts))); + if (ret.ok) { + *ret.u.ts = ts; + } + } + return ret; +} + +toml_datum_t toml_string_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + toml_raw_t raw = toml_raw_in(arr, key); + if (raw) { + ret.ok = (0 == toml_rtos(raw, &ret.u.s)); + } + return ret; +} + +toml_datum_t toml_bool_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtob(toml_raw_in(arr, key), &ret.u.b)); + return ret; +} + +toml_datum_t toml_int_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtoi(toml_raw_in(arr, key), &ret.u.i)); + return ret; +} + +toml_datum_t toml_double_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtod(toml_raw_in(arr, key), &ret.u.d)); + return ret; +} + +toml_datum_t toml_timestamp_in(const toml_table_t* arr, const char* key) +{ + toml_timestamp_t ts; + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtots(toml_raw_in(arr, key), &ts)); + if (ret.ok) { + ret.ok = !!(ret.u.ts = malloc(sizeof(*ret.u.ts))); + if (ret.ok) { + *ret.u.ts = ts; + } + } + return ret; +} diff --git a/instrumentation/nginx/src/toml.h b/instrumentation/nginx/src/toml.h new file mode 100644 index 000000000..19f6f6469 --- /dev/null +++ b/instrumentation/nginx/src/toml.h @@ -0,0 +1,175 @@ +/* + MIT License + + Copyright (c) 2017 - 2019 CK Tan + https://github.com/cktan/tomlc99 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#ifndef TOML_H +#define TOML_H + + +#include +#include + + +#ifdef __cplusplus +#define TOML_EXTERN extern "C" +#else +#define TOML_EXTERN extern +#endif + +typedef struct toml_timestamp_t toml_timestamp_t; +typedef struct toml_table_t toml_table_t; +typedef struct toml_array_t toml_array_t; +typedef struct toml_datum_t toml_datum_t; + +/* Parse a file. Return a table on success, or 0 otherwise. + * Caller must toml_free(the-return-value) after use. + */ +TOML_EXTERN toml_table_t* toml_parse_file(FILE* fp, + char* errbuf, + int errbufsz); + +/* Parse a string containing the full config. + * Return a table on success, or 0 otherwise. + * Caller must toml_free(the-return-value) after use. + */ +TOML_EXTERN toml_table_t* toml_parse(char* conf, /* NUL terminated, please. */ + char* errbuf, + int errbufsz); + +/* Free the table returned by toml_parse() or toml_parse_file(). Once + * this function is called, any handles accessed through this tab + * directly or indirectly are no longer valid. + */ +TOML_EXTERN void toml_free(toml_table_t* tab); + + +/* Timestamp types. The year, month, day, hour, minute, second, z + * fields may be NULL if they are not relevant. e.g. In a DATE + * type, the hour, minute, second and z fields will be NULLs. + */ +struct toml_timestamp_t { + struct { /* internal. do not use. */ + int year, month, day; + int hour, minute, second, millisec; + char z[10]; + } __buffer; + int *year, *month, *day; + int *hour, *minute, *second, *millisec; + char* z; +}; + + +/*----------------------------------------------------------------- + * Enhanced access methods + */ +struct toml_datum_t { + int ok; + union { + toml_timestamp_t* ts; /* ts must be freed after use */ + char* s; /* string value. s must be freed after use */ + int b; /* bool value */ + int64_t i; /* int value */ + double d; /* double value */ + } u; +}; + +/* on arrays: */ +/* ... retrieve size of array. */ +TOML_EXTERN int toml_array_nelem(const toml_array_t* arr); +/* ... retrieve values using index. */ +TOML_EXTERN toml_datum_t toml_string_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_bool_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_int_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_double_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_timestamp_at(const toml_array_t* arr, int idx); +/* ... retrieve array or table using index. */ +TOML_EXTERN toml_array_t* toml_array_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_table_t* toml_table_at(const toml_array_t* arr, int idx); + +/* on tables: */ +/* ... retrieve the key in table at keyidx. Return 0 if out of range. */ +TOML_EXTERN const char* toml_key_in(const toml_table_t* tab, int keyidx); +/* ... retrieve values using key. */ +TOML_EXTERN toml_datum_t toml_string_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_bool_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_int_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_double_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_timestamp_in(const toml_table_t* arr, const char* key); +/* .. retrieve array or table using key. */ +TOML_EXTERN toml_array_t* toml_array_in(const toml_table_t* tab, + const char* key); +TOML_EXTERN toml_table_t* toml_table_in(const toml_table_t* tab, + const char* key); + +/*----------------------------------------------------------------- + * lesser used + */ +/* Return the array kind: 't'able, 'a'rray, 'v'alue */ +TOML_EXTERN char toml_array_kind(const toml_array_t* arr); + +/* For array kind 'v'alue, return the type of values + i:int, d:double, b:bool, s:string, t:time, D:date, T:timestamp + 0 if unknown +*/ +TOML_EXTERN char toml_array_type(const toml_array_t* arr); + +/* Return the key of an array */ +TOML_EXTERN const char* toml_array_key(const toml_array_t* arr); + +/* Return the number of key-values in a table */ +TOML_EXTERN int toml_table_nkval(const toml_table_t* tab); + +/* Return the number of arrays in a table */ +TOML_EXTERN int toml_table_narr(const toml_table_t* tab); + +/* Return the number of sub-tables in a table */ +TOML_EXTERN int toml_table_ntab(const toml_table_t* tab); + +/* Return the key of a table*/ +TOML_EXTERN const char* toml_table_key(const toml_table_t* tab); + +/*-------------------------------------------------------------- + * misc + */ +TOML_EXTERN int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret); +TOML_EXTERN int toml_ucs_to_utf8(int64_t code, char buf[6]); +TOML_EXTERN void toml_set_memutil(void* (*xxmalloc)(size_t), + void (*xxfree)(void*)); + + +/*-------------------------------------------------------------- + * deprecated + */ +/* A raw value, must be processed by toml_rto* before using. */ +typedef const char* toml_raw_t; +TOML_EXTERN toml_raw_t toml_raw_in(const toml_table_t* tab, const char* key); +TOML_EXTERN toml_raw_t toml_raw_at(const toml_array_t* arr, int idx); +TOML_EXTERN int toml_rtos(toml_raw_t s, char** ret); +TOML_EXTERN int toml_rtob(toml_raw_t s, int* ret); +TOML_EXTERN int toml_rtoi(toml_raw_t s, int64_t* ret); +TOML_EXTERN int toml_rtod(toml_raw_t s, double* ret); +TOML_EXTERN int toml_rtod_ex(toml_raw_t s, double* ret, char* buf, int buflen); +TOML_EXTERN int toml_rtots(toml_raw_t s, toml_timestamp_t* ret); + + +#endif /* TOML_H */ diff --git a/instrumentation/nginx/src/trace_context.cpp b/instrumentation/nginx/src/trace_context.cpp new file mode 100644 index 000000000..6b935695b --- /dev/null +++ b/instrumentation/nginx/src/trace_context.cpp @@ -0,0 +1,77 @@ +#include "trace_context.h" +#include "nginx_utils.h" + +static TraceHeader* +FindEmptyOrExistingSlot(TraceContext* context, opentelemetry::nostd::string_view traceType) { + for (TraceHeader& slot : context->traceHeader) { + if (slot.key.len == 0) { + return &slot; + } + + if (slot.key.len == traceType.size() && ngx_strcmp(slot.key.data, traceType.data()) == 0) { + return &slot; + } + } + return nullptr; +} + +static bool IsEmpty(ngx_str_t s) { return s.len == 0; } + +ngx_str_t CreatePooledString(ngx_pool_t* pool, opentelemetry::nostd::string_view value) { + u_char* data = (u_char*)ngx_palloc(pool, value.size()); + + if (!data) { + return ngx_null_string; + } + + ngx_memcpy(data, value.data(), value.size()); + + return {value.size(), data}; +} + +bool TraceContextSetTraceHeader( + TraceContext* context, opentelemetry::nostd::string_view traceType, + opentelemetry::nostd::string_view traceValue) { + + if (traceType.empty()) { + return false; + } + + TraceHeader* slot = FindEmptyOrExistingSlot(context, traceType); + + if (!slot) { + return false; + } + + ngx_pool_t* pool = context->request->pool; + ngx_str_t key = CreatePooledString(pool, traceType); + + if (IsEmpty(key)) { + return false; + } + + ngx_str_t value = CreatePooledString(pool, traceValue); + + if (IsEmpty(value)) { + return false; + } + + slot->key = key; + slot->value = value; + return true; +} + +const TraceHeader* +TraceContextFindTraceHeader(const TraceContext* context, opentelemetry::nostd::string_view key) { + if (key.empty()) { + return nullptr; + } + + for (const TraceHeader& slot : context->traceHeader) { + if (slot.key.len == key.size() && ngx_strncmp(slot.key.data, key.data(), slot.key.len) == 0) { + return &slot; + } + } + + return nullptr; +} diff --git a/instrumentation/nginx/src/trace_context.h b/instrumentation/nginx/src/trace_context.h new file mode 100644 index 000000000..c43412d70 --- /dev/null +++ b/instrumentation/nginx/src/trace_context.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include "script.h" + +extern "C" { +#include +} + +struct TraceHeader { + ngx_str_t key = ngx_null_string; + ngx_str_t value = ngx_null_string; +}; + +enum TracePropagationType { + TracePropagationW3C, + TracePropagationB3, +}; + +struct TraceContext { + TraceContext(ngx_http_request_t* req) : request(req), traceHeader{} {} + /* The current request being handled by nginx. */ + ngx_http_request_t* request; + opentelemetry::nostd::shared_ptr request_span; + /* Headers to be injected for the upstream request. */ + TraceHeader traceHeader[2]; +}; + +bool TraceContextSetTraceHeader( + TraceContext* context, opentelemetry::nostd::string_view key, + opentelemetry::nostd::string_view value); + +const TraceHeader* +TraceContextFindTraceHeader(const TraceContext* context, opentelemetry::nostd::string_view key); diff --git a/instrumentation/nginx/test/Dockerfile b/instrumentation/nginx/test/Dockerfile new file mode 100644 index 000000000..85f004db2 --- /dev/null +++ b/instrumentation/nginx/test/Dockerfile @@ -0,0 +1,35 @@ +FROM ubuntu:20.04 + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive \ + TZ="Europe/London" \ + apt-get install --no-install-recommends --no-install-suggests -y \ + build-essential autoconf libtool pkg-config ca-certificates \ + cmake gcc g++ python3 git nginx libpcre3-dev + +RUN git clone --shallow-submodules --depth 1 --recurse-submodules -b v1.35.0 https://github.com/grpc/grpc \ + && cd grpc \ + && mkdir -p cmake/build \ + && cd cmake/build \ + && cmake -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF -DgRPC_BUILD_CSHARP_EXTENSIONS=OFF ../.. \ + && make -j2 \ + && make install + +RUN git clone --shallow-submodules --depth 1 --recurse-submodules \ + https://github.com/open-telemetry/opentelemetry-cpp.git \ + && cd opentelemetry-cpp \ + && mkdir build \ + && cd build \ + && cmake -DWITH_OTLP=ON -DBUILD_TESTING=OFF -DWITH_EXAMPLES=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON .. \ + && make -j2 \ + && make install + +RUN mkdir -p otel-nginx/build && mkdir -p otel-nginx/src +COPY src otel-nginx/src/ +COPY CMakeLists.txt nginx.cmake otel-nginx/ +RUN ls otel-nginx && cd otel-nginx/build \ + && cmake -DCMAKE_INSTALL_PREFIX=/usr/share/nginx/modules .. \ + && make -j2 \ + && make install + +CMD ["/usr/sbin/nginx", "-g", "daemon off;"] diff --git a/instrumentation/nginx/test/backend/files/content.txt b/instrumentation/nginx/test/backend/files/content.txt new file mode 100644 index 000000000..fc1c3cfb8 --- /dev/null +++ b/instrumentation/nginx/test/backend/files/content.txt @@ -0,0 +1 @@ +Lorem Ipsum \ No newline at end of file diff --git a/instrumentation/nginx/test/backend/php/app.php b/instrumentation/nginx/test/backend/php/app.php new file mode 100644 index 000000000..ca09a0096 --- /dev/null +++ b/instrumentation/nginx/test/backend/php/app.php @@ -0,0 +1,10 @@ + $traceparent))); +?> diff --git a/instrumentation/nginx/test/backend/php/b3.php b/instrumentation/nginx/test/backend/php/b3.php new file mode 100644 index 000000000..1cacb17e3 --- /dev/null +++ b/instrumentation/nginx/test/backend/php/b3.php @@ -0,0 +1,10 @@ + $b3))); +?> diff --git a/instrumentation/nginx/test/backend/simple_express/Dockerfile b/instrumentation/nginx/test/backend/simple_express/Dockerfile new file mode 100644 index 000000000..e76dd6b73 --- /dev/null +++ b/instrumentation/nginx/test/backend/simple_express/Dockerfile @@ -0,0 +1,6 @@ +FROM node:14-alpine + +COPY package.json package-lock.json index.js / +RUN npm install --production + +CMD ["node", "index.js"] diff --git a/instrumentation/nginx/test/backend/simple_express/index.js b/instrumentation/nginx/test/backend/simple_express/index.js new file mode 100644 index 000000000..2bfc9aba1 --- /dev/null +++ b/instrumentation/nginx/test/backend/simple_express/index.js @@ -0,0 +1,38 @@ +const express = require('express') +const app = express() +const port = 3500 + +const traceparentRegex = /00-[0-9a-f]{32}-[0-9a-f]{16}-00/; + +app.get('/', (req, res) => { + let traceparent = req.header("traceparent"); + if (!traceparentRegex.test(traceparent)) { + throw "Missing traceparent header"; + } + + res.json({traceparent: traceparent}); +}); + +app.get("/b3", (req, res) => { + let header = req.header("b3"); + if (!/[0-9a-f]{32}-[0-9a-f]{16}-[0-1]{1}/.test(header)) { + throw "Missing b3 header"; + } + + res.json({"b3": header}); +}); + +app.get("/off", (req, res) => { + if (req.header("traceparent") !== undefined) { + throw "Found traceparent header, but expected none"; + } + + res.json({}); +}); + +const server = app.listen(port, () => { + console.log(`simple_express ready at http://localhost:${port}`) +}); + +process.on("SIGTERM", () => server.close()); +process.on("SIGINT", () => server.close()); diff --git a/instrumentation/nginx/test/backend/simple_express/package-lock.json b/instrumentation/nginx/test/backend/simple_express/package-lock.json new file mode 100644 index 000000000..eddbf628c --- /dev/null +++ b/instrumentation/nginx/test/backend/simple_express/package-lock.json @@ -0,0 +1,374 @@ +{ + "name": "otdummye", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" + }, + "mime-types": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "requires": { + "mime-db": "1.45.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } +} diff --git a/instrumentation/nginx/test/backend/simple_express/package.json b/instrumentation/nginx/test/backend/simple_express/package.json new file mode 100644 index 000000000..019a2c4bd --- /dev/null +++ b/instrumentation/nginx/test/backend/simple_express/package.json @@ -0,0 +1,13 @@ +{ + "name": "otel-test", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.17.1" + } +} diff --git a/instrumentation/nginx/test/conf/collector.yml b/instrumentation/nginx/test/conf/collector.yml new file mode 100644 index 000000000..9580a6a93 --- /dev/null +++ b/instrumentation/nginx/test/conf/collector.yml @@ -0,0 +1,17 @@ +receivers: + otlp: + protocols: + grpc: +exporters: + logging: + logLevel: debug + file: + path: /trace.json +processors: + batch: +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [logging, file] diff --git a/instrumentation/nginx/test/conf/nginx.conf b/instrumentation/nginx/test/conf/nginx.conf new file mode 100644 index 000000000..363536e92 --- /dev/null +++ b/instrumentation/nginx/test/conf/nginx.conf @@ -0,0 +1,50 @@ +load_module modules/otel_ngx_module.so; + +events {} + +http { + opentelemetry_config /conf/otel-nginx.toml; + access_log stderr; + error_log stderr debug; + + upstream node-backend { + server node-backend:3500; + } + + server { + listen 8000; + server_name otel_test; + + root /var/www/html; + + location = / { + opentelemetry_operation_name simple_backend; + opentelemetry_propagate; + proxy_pass http://node-backend/; + } + + location = /b3 { + opentelemetry_operation_name test_b3; + opentelemetry_propagate b3; + proxy_pass http://node-backend/b3; + } + + location = /off { + opentelemetry off; + proxy_pass http://node-backend/off; + } + + location /files/ { + opentelemetry_operation_name file_access; + try_files $uri =404; + } + + location ~ \.php$ { + root /var/www/html/php; + opentelemetry_operation_name php_fpm_backend; + opentelemetry_propagate; + fastcgi_pass php-backend:9000; + include fastcgi.conf; + } + } +} diff --git a/instrumentation/nginx/test/conf/otel-nginx.toml b/instrumentation/nginx/test/conf/otel-nginx.toml new file mode 100644 index 000000000..2493539a9 --- /dev/null +++ b/instrumentation/nginx/test/conf/otel-nginx.toml @@ -0,0 +1,19 @@ +exporter = "otlp" +processor = "simple" + +[exporters.otlp] +host = "collector" +port = 4317 + +[exporters.jaeger] +host = "localhost" +port = 9090 +transport = "thrift_udp" + +[processors.batch] +max_queue_size = 2048 +schedule_delay_millis = 5000 +max_export_batch_size = 512 + +[service] +name = "nginx-proxy" diff --git a/instrumentation/nginx/test/data/trace.json b/instrumentation/nginx/test/data/trace.json new file mode 100644 index 000000000..e69de29bb diff --git a/instrumentation/nginx/test/docker-compose.yml b/instrumentation/nginx/test/docker-compose.yml new file mode 100644 index 000000000..d95546261 --- /dev/null +++ b/instrumentation/nginx/test/docker-compose.yml @@ -0,0 +1,28 @@ +version: "3" +services: + collector: + image: otel/opentelemetry-collector-contrib-dev:latest + volumes: + - ./conf/collector.yml:/etc/otel/config.yaml + - ./data/trace.json:/trace.json + nginx: + image: otel-nginx-test/nginx:latest + volumes: + - ./conf/nginx.conf:/etc/nginx/nginx.conf + - ./conf/otel-nginx.toml:/conf/otel-nginx.toml + - ./backend/files:/var/www/html/files + ports: + - "8000:8000" + command: + - /usr/sbin/nginx + - -g + - daemon off; + node-backend: + image: otel-nginx-test/express-backend:latest + command: node index.js + volumes: + - ./backend/simple_express:/app + php-backend: + image: bitnami/php-fpm:7.4-prod + volumes: + - ./backend/php/:/var/www/html/php diff --git a/instrumentation/nginx/test/instrumentation/.formatter.exs b/instrumentation/nginx/test/instrumentation/.formatter.exs new file mode 100644 index 000000000..d2cda26ed --- /dev/null +++ b/instrumentation/nginx/test/instrumentation/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/instrumentation/nginx/test/instrumentation/.gitignore b/instrumentation/nginx/test/instrumentation/.gitignore new file mode 100644 index 000000000..7d2134cb1 --- /dev/null +++ b/instrumentation/nginx/test/instrumentation/.gitignore @@ -0,0 +1,27 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +instrumentation-*.tar + + +# Temporary files for e.g. tests +/tmp diff --git a/instrumentation/nginx/test/instrumentation/mix.exs b/instrumentation/nginx/test/instrumentation/mix.exs new file mode 100644 index 000000000..f03e033c7 --- /dev/null +++ b/instrumentation/nginx/test/instrumentation/mix.exs @@ -0,0 +1,20 @@ +defmodule Instrumentation.MixProject do + use Mix.Project + + def project do + [ + app: :instrumentation, + version: "0.1.0", + elixir: "~> 1.11", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + defp deps do + [ + {:httpoison, "1.8.0"}, + {:jason, "1.2.2"} + ] + end +end diff --git a/instrumentation/nginx/test/instrumentation/mix.lock b/instrumentation/nginx/test/instrumentation/mix.lock new file mode 100644 index 000000000..857e573ca --- /dev/null +++ b/instrumentation/nginx/test/instrumentation/mix.lock @@ -0,0 +1,12 @@ +%{ + "certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"}, + "hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"}, + "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, +} diff --git a/instrumentation/nginx/test/instrumentation/test/instrumentation_test.exs b/instrumentation/nginx/test/instrumentation/test/instrumentation_test.exs new file mode 100644 index 000000000..cdffe17e8 --- /dev/null +++ b/instrumentation/nginx/test/instrumentation/test/instrumentation_test.exs @@ -0,0 +1,295 @@ +defmodule InstrumentationTest do + use ExUnit.Case + + @host "localhost:8000" + @traces_path "../data/trace.json" + + def has_line(lines, re) do + Enum.find(lines, fn line -> String.match?(line, re) end) != nil + end + + def wait_until_ready(_, %{:collector => true, :express => true}), do: :ready + + def wait_until_ready(port, ctx) do + receive do + {_, {:data, output}} -> + lines = String.split(output, "\n", trim: true) + + has_collector = ctx[:collector] || has_line(lines, ~r/everything is ready/i) + has_express = ctx[:express] || has_line(lines, ~r/simple_express ready/i) + + wait_until_ready( + port, + Map.merge(ctx, %{ + collector: has_collector, + express: has_express + }) + ) + after + 30_000 -> raise "Timed out waiting for docker containers" + end + end + + def wait_until_ready(port) do + wait_until_ready(port, %{}) + end + + def read_traces(_file, num_traces, _timeout, traces) when num_traces <= 0, do: traces + + def read_traces(_file, _num_traces, timeout, _traces) when timeout <= 0, + do: raise("timed out waiting for traces") + + def read_traces(file, num_traces, timeout, traces) do + case IO.read(file, :line) do + :eof -> + Process.sleep(100) + read_traces(file, num_traces, timeout - 100, traces) + + line -> + read_traces(file, num_traces - 1, timeout, [Jason.decode!(line) | traces]) + end + end + + def read_traces(file, num_traces, timeout \\ 1_000) do + read_traces(file, num_traces, timeout, []) + end + + def collect_spans(trace) do + [resource_spans] = trace["resourceSpans"] + [il_spans] = resource_spans["instrumentationLibrarySpans"] + il_spans["spans"] + end + + def attrib(span, key) do + case Enum.find(span["attributes"], fn %{"key" => k} -> k == key end) do + %{"value" => val} -> + [v] = Map.values(val) + v + + _ -> + nil + end + end + + def read_until_eof(file, lines) do + case IO.read(file, :line) do + :eof -> + lines + + line -> + read_until_eof(file, [line | lines]) + end + end + + def read_until_eof(file) do + read_until_eof(file, []) + end + + setup_all do + port = Port.open({:spawn, "docker-compose up"}, [:binary]) + + on_exit(fn -> System.cmd("docker-compose", ["down"]) end) + + wait_until_ready(port) + + trace_file = File.open!(@traces_path, [:read]) + + on_exit(fn -> + File.close(trace_file) + end) + + %{trace_file: trace_file} + end + + setup %{trace_file: trace_file} = ctx do + read_until_eof(trace_file) + ctx + end + + test "HTTP upstream | span attributes", %{trace_file: trace_file} do + %HTTPoison.Response{status_code: status} = HTTPoison.get!("#{@host}/?foo=bar&x=42") + + [trace] = read_traces(trace_file, 1) + [span] = collect_spans(trace) + + assert status == 200 + + assert attrib(span, "http.method") == "GET" + assert attrib(span, "http.flavor") == "1.1" + assert attrib(span, "http.target") == "/?foo=bar&x=42" + assert attrib(span, "http.host") == @host + assert attrib(span, "http.server_name") == "otel_test" + assert attrib(span, "http.scheme") == "http" + assert attrib(span, "http.status_code") == "200" + + assert span["kind"] == "SPAN_KIND_SERVER" + assert span["name"] == "simple_backend" + end + + def test_parent_span(url, %{trace_file: trace_file}) do + parent_span_id = "2a9d49c3e3b7c461" + input_trace_id = "aad85b4f655feed4d594a01cfa6a1d62" + + %HTTPoison.Response{status_code: status, body: body} = + HTTPoison.get!(url, [ + {"traceparent", "00-#{input_trace_id}-#{parent_span_id}-00"} + ]) + + %{"traceparent" => traceparent} = Jason.decode!(body) + ["00", trace_id, span_id, "00"] = String.split(traceparent, "-") + + [trace] = read_traces(trace_file, 1) + [span] = collect_spans(trace) + + assert status == 200 + assert trace_id == input_trace_id + assert span_id != parent_span_id + assert String.length(span_id) == 16 + + assert span["parentSpanId"] == parent_span_id + assert span["spanId"] != parent_span_id + end + + test "HTTP upstream | span is created when no traceparent exists", %{trace_file: trace_file} do + %HTTPoison.Response{status_code: status} = HTTPoison.get!(@host) + + [trace] = read_traces(trace_file, 1) + [span] = collect_spans(trace) + + assert status == 200 + assert span["parentSpanId"] == "" + assert attrib(span, "http.status_code") == "200" + end + + test "HTTP upstream | span is associated with parent", ctx do + test_parent_span(@host, ctx) + end + + test "PHP-FPM upstream | span attributes", %{trace_file: trace_file} do + %HTTPoison.Response{status_code: status} = HTTPoison.get!("#{@host}/app.php") + + [trace] = read_traces(trace_file, 1) + [span] = collect_spans(trace) + + assert status == 200 + assert attrib(span, "http.method") == "GET" + assert attrib(span, "http.flavor") == "1.1" + assert attrib(span, "http.target") == "/app.php" + assert attrib(span, "http.host") == @host + assert attrib(span, "http.server_name") == "otel_test" + assert attrib(span, "http.scheme") == "http" + assert attrib(span, "http.status_code") == "200" + + assert span["parentSpanId"] == "" + assert span["kind"] == "SPAN_KIND_SERVER" + assert span["name"] == "php_fpm_backend" + end + + test "PHP-FPM upstream | span is associated with parent", ctx do + test_parent_span("#{@host}/app.php", ctx) + end + + test "HTTP upstream | test b3 injection", %{trace_file: trace_file} do + %HTTPoison.Response{status_code: status} = HTTPoison.get!("#{@host}/b3") + + [trace] = read_traces(trace_file, 1) + [span] = collect_spans(trace) + + assert status == 200 + assert span["parentSpanId"] == "" + assert span["name"] == "test_b3" + end + + test "PHP-FPM upstream | test b3 injection", %{trace_file: trace_file} do + %HTTPoison.Response{status_code: status} = HTTPoison.get!("#{@host}/b3") + + [trace] = read_traces(trace_file, 1) + [span] = collect_spans(trace) + + assert status == 200 + assert span["parentSpanId"] == "" + assert span["name"] == "test_b3" + end + + test "HTTP upstream | test b3 propagation", %{trace_file: trace_file} do + parent_span_id = "2a9d49c3e3b7c461" + input_trace_id = "aad85b4f655feed4d594a01cfa6a1d62" + + %HTTPoison.Response{status_code: status, body: body} = + HTTPoison.get!("#{@host}/b3", [ + {"b3", "#{input_trace_id}-#{parent_span_id}-1"} + ]) + + [trace] = read_traces(trace_file, 1) + [span] = collect_spans(trace) + + %{"b3" => b3} = Jason.decode!(body) + [trace_id, span_id, _] = String.split(b3, "-") + assert trace_id == input_trace_id + assert span_id != parent_span_id + + assert status == 200 + assert span["parentSpanId"] == parent_span_id + assert span["spanId"] != parent_span_id + assert span["name"] == "test_b3" + end + + test "HTTP upstream | multiheader b3 propagation", %{trace_file: trace_file} do + parent_span_id = "2a9d49c3e3b7c461" + input_trace_id = "aad85b4f655feed4d594a01cfa6a1d62" + + %HTTPoison.Response{status_code: status, body: body} = + HTTPoison.get!("#{@host}/b3", [ + {"X-B3-TraceId", input_trace_id}, + {"X-B3-SpanId", parent_span_id}, + {"X-B3-Sampled", "1"} + ]) + + [trace] = read_traces(trace_file, 1) + [span] = collect_spans(trace) + + %{"b3" => b3} = Jason.decode!(body) + [trace_id, span_id, _] = String.split(b3, "-") + assert trace_id == input_trace_id + assert span_id != parent_span_id + + assert status == 200 + assert span["parentSpanId"] == parent_span_id + assert span["spanId"] != parent_span_id + assert span["name"] == "test_b3" + end + + test "Accessing a file produces a span", %{trace_file: trace_file} do + %HTTPoison.Response{status_code: status, body: body} = + HTTPoison.get!("#{@host}/files/content.txt") + + [trace] = read_traces(trace_file, 1) + [span] = collect_spans(trace) + + assert body == "Lorem Ipsum" + assert status == 200 + assert attrib(span, "http.method") == "GET" + assert attrib(span, "http.flavor") == "1.1" + assert attrib(span, "http.target") == "/files/content.txt" + assert attrib(span, "http.host") == @host + assert attrib(span, "http.server_name") == "otel_test" + assert attrib(span, "http.scheme") == "http" + assert attrib(span, "http.status_code") == "200" + + assert span["parentSpanId"] == "" + assert span["kind"] == "SPAN_KIND_SERVER" + assert span["name"] == "file_access" + end + + test "Accessing a route with disabled OpenTelemetry does not produce spans nor propagate", %{ + trace_file: trace_file + } do + %HTTPoison.Response{status_code: status} = HTTPoison.get!("#{@host}/off") + + assert_raise RuntimeError, "timed out waiting for traces", fn -> + read_traces(trace_file, 1) + end + + assert status == 200 + end +end diff --git a/instrumentation/nginx/test/instrumentation/test/test_helper.exs b/instrumentation/nginx/test/instrumentation/test/test_helper.exs new file mode 100644 index 000000000..869559e70 --- /dev/null +++ b/instrumentation/nginx/test/instrumentation/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()