Compare commits

...

8 commits

Author SHA1 Message Date
daroche
9400f5a869 hacks to build on nix 2025-08-05 19:07:59 +02:00
Eugene Grebenschikov
38cae30841 Verify export via TLS (#12).
Co-authored-by: Pavel Pautov <p.pautov@f5.com>
2025-02-11 12:49:16 -05:00
Pavel Pautov
0aec04198c Verify custom exporter headers support (#62). 2025-02-11 12:49:16 -05:00
Pavel Pautov
d0385c8f68 Support sending custom headers to export endpoint (fix #62).
The headers are configured by "header" directive in "otel_exporter" block, e.g.
    otel_exporter {
        endpoint localhost:4317;
        header X-API-Token "token value";
    }
2025-02-11 12:49:16 -05:00
Pavel Pautov
a23ffa955c Consolidate transport related parameters into a struct.
Also, replace leftover cast with getMainConf().
2025-02-11 12:49:16 -05:00
Pavel Pautov
3b6667c808 Fail early if "trusted_certificate" is a directory.
Previously, the error was caused by enormous std::string allocation.
2025-02-11 12:49:16 -05:00
Eugene
323e7fd328 Verify custom resource attributes support (#32).
Co-authored-by: p-pautov <37922380+p-pautov@users.noreply.github.com>
2025-02-11 12:49:16 -05:00
Sergey A. Osokin
3a655dfa8a Remove cmake from the build process
While I'm here change the name of the module to ngx_otel_module.
2024-12-19 17:07:23 -05:00
11 changed files with 520 additions and 252 deletions

View file

@ -1,159 +0,0 @@
cmake_minimum_required(VERSION 3.16.3)
project(nginx-otel)
set(NGX_OTEL_NGINX_BUILD_DIR ""
CACHE PATH "Nginx build (objs) dir")
set(NGX_OTEL_NGINX_DIR "${NGX_OTEL_NGINX_BUILD_DIR}/.."
CACHE PATH "Nginx source dir")
set(NGX_OTEL_GRPC e241f37befe7ba4688effd84bfbf99b0f681a2f7 # v1.49.4
CACHE STRING "gRPC tag to download or 'package' to use preinstalled")
set(NGX_OTEL_SDK 11d5d9e0d8fd8ba876c8994714cc2647479b6574 # v1.11.0
CACHE STRING "OTel SDK tag to download or 'package' to use preinstalled")
set(NGX_OTEL_PROTO_DIR "" CACHE PATH "OTel proto files root")
set(NGX_OTEL_DEV OFF CACHE BOOL "Enforce compiler warnings")
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
if(NGX_OTEL_GRPC STREQUAL "package")
find_package(protobuf REQUIRED)
find_package(gRPC REQUIRED)
else()
include(FetchContent)
FetchContent_Declare(
grpc
GIT_REPOSITORY https://github.com/grpc/grpc
GIT_TAG ${NGX_OTEL_GRPC}
GIT_SUBMODULES third_party/protobuf third_party/abseil-cpp third_party/re2
GIT_SHALLOW ON)
set(gRPC_USE_PROTO_LITE ON CACHE INTERNAL "")
set(gRPC_INSTALL OFF CACHE INTERNAL "")
set(gRPC_USE_SYSTEMD OFF CACHE INTERNAL "")
set(gRPC_DOWNLOAD_ARCHIVES OFF CACHE INTERNAL "")
set(gRPC_CARES_PROVIDER package CACHE INTERNAL "")
set(gRPC_SSL_PROVIDER package CACHE INTERNAL "")
set(gRPC_ZLIB_PROVIDER package CACHE INTERNAL "")
set(protobuf_INSTALL OFF CACHE INTERNAL "")
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
FetchContent_MakeAvailable(grpc)
# reconsider once https://github.com/grpc/grpc/issues/36023 is done
target_compile_definitions(grpc PRIVATE GRPC_NO_XDS GRPC_NO_RLS)
set_property(DIRECTORY ${grpc_SOURCE_DIR}
PROPERTY EXCLUDE_FROM_ALL YES)
add_library(gRPC::grpc++ ALIAS grpc++)
add_executable(gRPC::grpc_cpp_plugin ALIAS grpc_cpp_plugin)
endif()
if(NGX_OTEL_SDK STREQUAL "package")
find_package(opentelemetry-cpp REQUIRED)
else()
include(FetchContent)
FetchContent_Declare(
otelcpp
GIT_REPOSITORY https://github.com/open-telemetry/opentelemetry-cpp
GIT_TAG ${NGX_OTEL_SDK}
GIT_SUBMODULES third_party/opentelemetry-proto
GIT_SHALLOW ON)
set(BUILD_TESTING OFF CACHE INTERNAL "")
set(WITH_EXAMPLES OFF CACHE INTERNAL "")
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_POLICY_DEFAULT_CMP0063 NEW)
FetchContent_MakeAvailable(otelcpp)
set_property(DIRECTORY ${otelcpp_SOURCE_DIR}
PROPERTY EXCLUDE_FROM_ALL YES)
if(NOT NGX_OTEL_PROTO_DIR)
set(NGX_OTEL_PROTO_DIR
"${otelcpp_SOURCE_DIR}/third_party/opentelemetry-proto")
endif()
add_library(opentelemetry-cpp::trace ALIAS opentelemetry_trace)
endif()
set(PROTO_DIR ${NGX_OTEL_PROTO_DIR})
set(PROTOS
"${PROTO_DIR}/opentelemetry/proto/common/v1/common.proto"
"${PROTO_DIR}/opentelemetry/proto/resource/v1/resource.proto"
"${PROTO_DIR}/opentelemetry/proto/trace/v1/trace.proto"
"${PROTO_DIR}/opentelemetry/proto/collector/trace/v1/trace_service.proto")
set(PROTO_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
set(PROTO_SOURCES
"${PROTO_OUT_DIR}/opentelemetry/proto/common/v1/common.pb.cc"
"${PROTO_OUT_DIR}/opentelemetry/proto/resource/v1/resource.pb.cc"
"${PROTO_OUT_DIR}/opentelemetry/proto/trace/v1/trace.pb.cc"
"${PROTO_OUT_DIR}/opentelemetry/proto/collector/trace/v1/trace_service.pb.cc"
"${PROTO_OUT_DIR}/opentelemetry/proto/collector/trace/v1/trace_service.grpc.pb.cc")
# generate protobuf code for lite runtime
add_custom_command(
OUTPUT ${PROTO_SOURCES}
COMMAND protobuf::protoc
--proto_path ${PROTO_DIR}
--cpp_out lite:${PROTO_OUT_DIR}
--grpc_out ${PROTO_OUT_DIR}
--plugin protoc-gen-grpc=$<TARGET_FILE:gRPC::grpc_cpp_plugin>
${PROTOS}
# remove inconsequential UTF8 check during serialization to aid performance
COMMAND sed -i.bak -E
-e [[/ ::(PROTOBUF_NAMESPACE_ID|google::protobuf)::internal::WireFormatLite::VerifyUtf8String\(/,/\);/d]]
${PROTO_SOURCES}
DEPENDS ${PROTOS} protobuf::protoc gRPC::grpc_cpp_plugin
VERBATIM)
if (NGX_OTEL_DEV)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
add_compile_options(-Wall -Wtype-limits -Werror)
endif()
add_library(ngx_otel_module MODULE
src/http_module.cpp
src/grpc_log.cpp
src/modules.c
${PROTO_SOURCES})
# avoid 'lib' prefix in binary name
set_target_properties(ngx_otel_module PROPERTIES PREFIX "")
# can't use OTel's WITH_ABSEIL until cmake 3.24, as it triggers find_package()
target_compile_definitions(ngx_otel_module PRIVATE HAVE_ABSEIL)
if (APPLE)
target_link_options(ngx_otel_module PRIVATE -undefined dynamic_lookup)
endif()
target_include_directories(ngx_otel_module PRIVATE
${NGX_OTEL_NGINX_BUILD_DIR}
${NGX_OTEL_NGINX_DIR}/src/core
${NGX_OTEL_NGINX_DIR}/src/event
${NGX_OTEL_NGINX_DIR}/src/event/modules
${NGX_OTEL_NGINX_DIR}/src/event/quic
${NGX_OTEL_NGINX_DIR}/src/os/unix
${NGX_OTEL_NGINX_DIR}/src/http
${NGX_OTEL_NGINX_DIR}/src/http/modules
${NGX_OTEL_NGINX_DIR}/src/http/v2
${NGX_OTEL_NGINX_DIR}/src/http/v3
${PROTO_OUT_DIR})
target_link_libraries(ngx_otel_module
opentelemetry-cpp::trace
gRPC::grpc++)

View file

@ -122,7 +122,7 @@ Follow these steps to build the `ngx_otel_module` dynamic module on Ubuntu or De
Install build tools and dependencies.
```bash
sudo apt install cmake build-essential libssl-dev zlib1g-dev libpcre3-dev
sudo apt install build-essential libssl-dev zlib1g-dev libpcre3-dev
sudo apt install pkg-config libc-ares-dev libre2-dev # for gRPC
```
@ -134,13 +134,6 @@ For the next step, you will need the `configure` script that is packaged with th
git clone https://github.com/nginx/nginx.git
```
Configure NGINX to generate files necessary for dynamic module compilation. These files will be placed into the `nginx/objs` directory.
**Important:** If you did not obtain NGINX source code via the clone method in the previous step, you will need to adjust paths in the following commands to conform to your specific directory structure.
```bash
cd nginx
auto/configure --with-compat
```
Exit the NGINX directory and clone the `ngx_otel_module` repository.
```bash
@ -148,14 +141,24 @@ cd ..
git clone https://github.com/nginxinc/nginx-otel.git
```
Configure and build the NGINX OTel module.
**Important**: replace the path in the `cmake` command with the path to the `nginx/objs` directory from above.
Set up `NGX_OTEL_PROTO_DIR` variable to point it into root of `opentelemetry-proto`:
```bash
cd nginx-otel
mkdir build
cd build
cmake -DNGX_OTEL_NGINX_BUILD_DIR=/path/to/configured/nginx/objs ..
export NGX_OTEL_PROTO_DIR=/opt/local/include
```
Configure and build the NGINX OTel module as usual:
- for a built-in module:
```bash
cd nginx
./configure --with-http_ssl_module --add-module=../nginx-otel
make
```
- for a dynamic module:
```bash
cd nginx
./configure --with-http_ssl_module --add-dynamic-module=../nginx-otel
make
```

261
config
View file

@ -1,9 +1,256 @@
ngx_addon_name=ngx_otel_module
ngx_module_type=HTTP
ngx_module_name=$ngx_addon_name
ngx_module_incs=
ngx_module_deps=" \
$ngx_addon_dir/src/batch_exporter.hpp \
$ngx_addon_dir/src/grpc_log.hpp \
$ngx_addon_dir/src/ngx.hpp \
$ngx_addon_dir/src/str_view.hpp \
$ngx_addon_dir/src/trace_context.hpp \
$ngx_addon_dir/src/trace_service_client.hpp \
"
ngx_module_srcs=" \
$ngx_addon_dir/src/grpc_log.cpp \
$ngx_addon_dir/src/ngx_otel_module.cpp \
objs/opentelemetry/proto/common/v1/common.pb.cc \
objs/opentelemetry/proto/resource/v1/resource.pb.cc \
objs/opentelemetry/proto/trace/v1/trace.pb.cc \
objs/opentelemetry/proto/collector/trace/v1/trace_service.pb.cc \
objs/opentelemetry/proto/collector/trace/v1/trace_service.grpc.pb.cc \
"
cmake -D NGX_OTEL_NGINX_BUILD_DIR=$NGX_OBJS \
-D CMAKE_LIBRARY_OUTPUT_DIRECTORY=$PWD/$NGX_OBJS \
-D "CMAKE_C_FLAGS=$NGX_CC_OPT" \
-D "CMAKE_CXX_FLAGS=$NGX_CC_OPT" \
-D "CMAKE_MODULE_LINKER_FLAGS=$NGX_LD_OPT" \
$NGX_OTEL_CMAKE_OPTS \
-S $ngx_addon_dir -B $NGX_OBJS/otel || exit 1
ngx_feature="c-ares"
ngx_feature_name=""
ngx_feature_run=no
ngx_feature_incs="#include <ares.h>"
ngx_feature_path="/usr/include"
ngx_feature_libs="-lcares"
ngx_feature_test="ares_version(NULL);"
. auto/feature
if [ $ngx_found = no ]; then
# FreeBSD port
ngx_feature="libcares in /usr/local/"
ngx_feature_path="/usr/local/include"
if [ $NGX_RPATH = YES ]; then
ngx_feature_libs="-R/usr/local/lib -L/usr/local/lib -lcares"
else
ngx_feature_libs="-L/usr/local/lib -lcares"
fi
. auto/feature
fi
if [ $ngx_found = yes ]; then
ngx_module_libs="$ngx_module_libs -lcares"
fi
unset ngx_found
### C++ feature test is unavailable in nginx
CXX=${CXX:-c++}
CXXFLAGS="$CXXFLAGS --std=c++17"
CXX_TEST_FLAGS=${CXX_TEST_FLAGS:---std=c++17}
autocppfeature()
{
echo $ngx_n "checking for $ngx_feature ...$ngx_c"
cat << END >> $NGX_AUTOCONF_ERR
----------------------------------------
checking for $ngx_feature
END
ngx_found=no
if test -n "$ngx_feature_name"; then
ngx_have_feature=`echo $ngx_feature_name \
| tr abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ`
fi
if test -n "$ngx_feature_path"; then
for ngx_temp in $ngx_feature_path; do
ngx_feature_inc_path="$ngx_feature_inc_path -I $ngx_temp"
done
fi
cat << END > $NGX_AUTOTEST.cpp
$ngx_feature_incs
int main(void) {
$ngx_feature_test;
return 0;
}
END
ngx_test="$CXX $CXX_TEST_FLAGS $CXX_AUX_FLAGS $ngx_feature_inc_path \
-o $NGX_AUTOTEST $NGX_AUTOTEST.cpp $NGX_TEST_LD_OPT $ngx_feature_libs"
ngx_feature_inc_path=
eval "/bin/sh -c \"$ngx_test\" >> $NGX_AUTOCONF_ERR 2>&1"
if [ -x $NGX_AUTOTEST ]; then
echo " found"
ngx_found=yes
if test -n "$ngx_feature_name"; then
have=$ngx_have_feature . auto/have
fi
else
echo " not found"
echo "----------" >> $NGX_AUTOCONF_ERR
cat $NGX_AUTOTEST.cpp >> $NGX_AUTOCONF_ERR
echo "----------" >> $NGX_AUTOCONF_ERR
echo $ngx_test >> $NGX_AUTOCONF_ERR
echo "----------" >> $NGX_AUTOCONF_ERR
fi
rm -rf $NGX_AUTOTEST*
}
ngx_feature="re2"
ngx_feature_name=""
ngx_feature_run=no
ngx_feature_incs="#include <re2/re2.h>"
ngx_feature_path="/usr/include"
ngx_feature_libs="-lre2"
ngx_feature_test="RE2::FullMatch(\"hello\", \"h.*o\");"
autocppfeature
if [ $ngx_found = no ]; then
# FreeBSD port
ngx_feature="re2 in /usr/local/"
ngx_feature_path="/usr/local/include"
if [ $NGX_RPATH = YES ]; then
ngx_feature_libs="-R/usr/local/lib -L/usr/local/lib -lre2"
else
ngx_feature_libs="-L/usr/local/lib -lre2"
fi
autocppfeature
fi
if [ $ngx_found = yes ]; then
ngx_module_libs="$ngx_module_libs -lre2"
fi
ngx_feature="opentelemetry-cpp"
ngx_feature_name=""
ngx_feature_run=no
ngx_feature_incs="#include <opentelemetry/nostd/string_view.h>
typedef opentelemetry::nostd::string_view StrView;"
ngx_feature_path="/usr/include"
ngx_feature_libs="-lopentelemetry_common"
ngx_feature_test="using namespace std;
StrView str, prefix;
str.substr(0, prefix.size());"
autocppfeature
if [ $ngx_found = no ]; then
# FreeBSD port
ngx_feature="opentelemetry in /usr/local/"
ngx_feature_path="/usr/local/include"
if [ $NGX_RPATH = YES ]; then
ngx_feature_libs="-R/usr/local/lib -L/usr/local/lib -lopentelemetry_common"
else
ngx_feature_libs="-L/usr/local/lib -lopentelemetry_common -lopentelemetry_resources -lopentelemetry_trace"
fi
autocppfeature
fi
if [ $ngx_found = yes ]; then
ngx_module_libs="$ngx_module_libs -lopentelemetry_common -lopentelemetry_resources -lopentelemetry_trace $(pkg-config --libs absl_log absl_log_internal_check_op absl_cord absl_log_initialize) -lstdc++"
fi
#if [ ! -d $prefix/include/opentelemetry/proto ]; then
# echo "Need to install opentelemetry-proto."
# exit 2
#fi
ngx_feature="protobuf"
ngx_feature_name=""
ngx_feature_run=no
ngx_feature_incs="#include <google/protobuf/stubs/common.h>"
ngx_feature_path="/usr/include"
ngx_feature_libs="-lprotobuf"
ngx_feature_test="using namespace google::protobuf;
google::protobuf::internal::VersionString(0);"
autocppfeature
if [ $ngx_found = no ]; then
# FreeBSD port
ngx_feature="protobuf in /usr/local/"
ngx_feature_path="/usr/local/include"
if [ $NGX_RPATH = YES ]; then
ngx_feature_libs="-R/usr/local/lib -L/usr/local/lib -lprotobuf"
else
ngx_feature_libs="-L/usr/local/lib -lprotobuf"
fi
autocppfeature
fi
if [ $ngx_found = yes ]; then
ngx_module_libs="$ngx_module_libs -lprotobuf"
fi
ngx_feature="grpc"
ngx_feature_name=""
ngx_feature_run=no
ngx_feature_incs="#include <grpc/support/log.h>"
ngx_feature_path="/usr/include"
ngx_feature_libs="-lgrpc -lgpr -lgrpc++"
ngx_feature_test="gpr_log_verbosity_init();"
autocppfeature
if [ $ngx_found = no ]; then
# FreeBSD port
ngx_feature="grpc in /usr/local/"
ngx_feature_path="/usr/local/include"
if [ $NGX_RPATH = YES ]; then
ngx_feature_libs="-R/usr/local/lib -L/usr/local/lib -lgrpc -lgpr -lgrpc++"
else
ngx_feature_libs="-L/usr/local/lib -lgrpc -lgpr -lgrpc++"
fi
autocppfeature
fi
if [ $ngx_found = yes ]; then
ngx_module_libs="$ngx_module_libs -lgrpc -lgpr -lgrpc++"
fi
ngx_module_libs="$ngx_module_libs -lz -lm -lrt -lssl -lcrypto"
. auto/module
CORE_INCS="$CORE_INCS $ngx_feature_path"
OTEL_NGX_SRCS="$ngx_module_srcs"

View file

@ -1,10 +1,45 @@
cat << END >> $NGX_MAKEFILE
if [ ! "`which protoc 2>/dev/null`" ]; then
echo "Need to install protoc."
exit 2
else
PROTOC=`which protoc`
fi
modules: ngx_otel_module
if [ ! "`which grpc_cpp_plugin 2>/dev/null`" ]; then
echo "Need to install grpc tools."
exit 2
else
GRPC_CPP=`which grpc_cpp_plugin`
fi
ngx_otel_module:
\$(MAKE) -C $NGX_OBJS/otel
mkdir -p objs
.PHONY: ngx_otel_module
if [ -z $NGX_OTEL_PROTO_DIR ]; then
echo "Need to set \$NGX_OTEL_PROTO_DIR variable."
exit 2
fi
END
if [ ! -d $NGX_OTEL_PROTO_DIR ]; then
echo "\$NGX_OTEL_PROTO_DIR is set to unavailable directory."
exit 2
fi
find $NGX_OTEL_PROTO_DIR/opentelemetry/proto -type f -name '*.proto' | \
xargs $PROTOC \
--proto_path $NGX_OTEL_PROTO_DIR \
--cpp_out=objs \
--grpc_out=objs \
--plugin=protoc-gen-grpc=$GRPC_CPP
find objs -name '*.pb.cc' | \
xargs sed -i.bak -e "/ ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::VerifyUtf8String(/,/);/d"
for src_file in $OTEL_NGX_SRCS; do
obj_file="$NGX_OBJS/addon/src/`basename $src_file .cpp`.o"
echo "$obj_file : CFLAGS += $CXXFLAGS -Wno-missing-field-initializers -Wno-conditional-uninitialized -fPIC -fvisibility=hidden -DHAVE_ABSEIL -Dngx_otel_module_EXPORTS" >> $NGX_MAKEFILE
done
for src_file in $OTEL_NGX_SRCS; do
obj_file="$NGX_OBJS/addon/v1/`basename $src_file .cc`.o"
echo "$obj_file : CFLAGS += $CXXFLAGS -Wno-missing-field-initializers -Wno-conditional-uninitialized -fPIC -fvisibility=hidden -DHAVE_ABSEIL -Dngx_otel_module_EXPORTS" >> $NGX_MAKEFILE
done

View file

@ -111,10 +111,10 @@ public:
int attrSize{0};
};
BatchExporter(StrView target, bool ssl, const std::string& trustedCert,
BatchExporter(const Target& target,
size_t batchSize, size_t batchCount,
const std::map<StrView, StrView>& resourceAttrs) :
batchSize(batchSize), client(std::string(target), ssl, trustedCert)
batchSize(batchSize), client(target)
{
free.reserve(batchCount);
while (batchCount-- > 0) {

View file

@ -1,14 +0,0 @@
#include <ngx_config.h>
#include <ngx_core.h>
extern ngx_module_t gHttpModule;
ngx_module_t* ngx_modules[] = {
&gHttpModule,
NULL
};
char* ngx_module_names[] = {
"ngx_http_otel_module",
NULL
};

View file

@ -8,7 +8,7 @@
#include <fstream>
extern ngx_module_t gHttpModule;
extern ngx_module_t ngx_otel_module;
namespace {
@ -30,6 +30,7 @@ struct MainConf : MainConfBase {
std::map<StrView, StrView> resourceAttrs;
bool ssl;
std::string trustedCert;
Target::HeaderVec headers;
};
struct SpanAttr {
@ -49,6 +50,7 @@ char* setExporter(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
char* addResourceAttr(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
char* addSpanAttr(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
char* setTrustedCertificate(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
char* addExporterHeader(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
namespace Propagation {
@ -120,6 +122,10 @@ ngx_command_t gExporterCommands[] = {
NGX_CONF_TAKE1,
setTrustedCertificate },
{ ngx_string("header"),
NGX_CONF_TAKE2,
addExporterHeader },
{ ngx_string("interval"),
NGX_CONF_TAKE1,
ngx_conf_set_msec_slot,
@ -168,18 +174,18 @@ bool iremovePrefix(ngx_str_t* str, StrView p)
MainConf* getMainConf(ngx_conf_t* cf)
{
return static_cast<MainConf*>(
(MainConfBase*)ngx_http_conf_get_module_main_conf(cf, gHttpModule));
(MainConfBase*)ngx_http_conf_get_module_main_conf(cf, ngx_otel_module));
}
MainConf* getMainConf(ngx_cycle_t* cycle)
{
return static_cast<MainConf*>(
(MainConfBase*)ngx_http_cycle_get_module_main_conf(cycle, gHttpModule));
(MainConfBase*)ngx_http_cycle_get_module_main_conf(cycle, ngx_otel_module));
}
LocationConf* getLocationConf(ngx_http_request_t* r)
{
return (LocationConf*)ngx_http_get_module_loc_conf(r, gHttpModule);
return (LocationConf*)ngx_http_get_module_loc_conf(r, ngx_otel_module);
}
void cleanupOtelCtx(void* data)
@ -188,7 +194,7 @@ void cleanupOtelCtx(void* data)
OtelCtx* getOtelCtx(ngx_http_request_t* r)
{
auto ctx = (OtelCtx*)ngx_http_get_module_ctx(r, gHttpModule);
auto ctx = (OtelCtx*)ngx_http_get_module_ctx(r, ngx_otel_module);
// restore module context if it was reset by e.g. internal redirect
if (ctx == NULL && (r->internal || r->filter_finalize)) {
@ -196,7 +202,7 @@ OtelCtx* getOtelCtx(ngx_http_request_t* r)
for (auto cln = r->pool->cleanup; cln; cln = cln->next) {
if (cln->handler == cleanupOtelCtx) {
ctx = (OtelCtx*)cln->data;
ngx_http_set_ctx(r, ctx, gHttpModule);
ngx_http_set_ctx(r, ctx, ngx_otel_module);
break;
}
}
@ -217,7 +223,7 @@ OtelCtx* createOtelCtx(ngx_http_request_t* r)
storage->handler = cleanupOtelCtx;
auto ctx = new (storage->data) OtelCtx{};
ngx_http_set_ctx(r, ctx, gHttpModule);
ngx_http_set_ctx(r, ctx, ngx_otel_module);
return ctx;
}
@ -576,10 +582,14 @@ ngx_int_t initWorkerProcess(ngx_cycle_t* cycle)
}
try {
Target target;
target.endpoint = std::string(toStrView(mcf->endpoint));
target.ssl = mcf->ssl;
target.trustedCert = mcf->trustedCert;
target.headers = mcf->headers;
gExporter.reset(new BatchExporter(
toStrView(mcf->endpoint),
mcf->ssl,
mcf->trustedCert,
target,
mcf->batchSize,
mcf->batchCount,
mcf->resourceAttrs));
@ -648,7 +658,7 @@ char* setExporter(ngx_conf_t* cf, ngx_command_t* cmd, void* conf)
continue;
}
if (cf->args->nelts != 2) {
if (cf->args->nelts != static_cast<unsigned>(ffs(cmd->type))) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid number of arguments in \"%V\" "
"directive of \"otel_exporter\"", name);
@ -711,7 +721,8 @@ char* addResourceAttr(ngx_conf_t* cf, ngx_command_t* cmd, void* conf)
return NGX_CONF_OK;
}
char* setTrustedCertificate(ngx_conf_t* cf, ngx_command_t* cmd, void* conf) {
char* setTrustedCertificate(ngx_conf_t* cf, ngx_command_t* cmd, void* conf)
{
auto path = ((ngx_str_t*)cf->args->elts)[1];
auto mcf = getMainConf(cf);
@ -727,11 +738,13 @@ char* setTrustedCertificate(ngx_conf_t* cf, ngx_command_t* cmd, void* conf) {
return (char*)NGX_CONF_ERROR;
}
file.exceptions(std::ios::failbit | std::ios::badbit);
file.seekg(0, std::ios::end);
size_t size = file.tellg();
mcf->trustedCert.resize(size);
file.peek(); // trigger early error for dirs
size_t size = file.seekg(0, std::ios::end).tellg();
file.seekg(0);
file.read(&mcf->trustedCert[0], mcf->trustedCert.size());
mcf->trustedCert.resize(size);
file.read(&mcf->trustedCert[0], size);
} catch (const std::exception& e) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"failed to read \"%V\": %s", &path, e.what());
@ -741,6 +754,33 @@ char* setTrustedCertificate(ngx_conf_t* cf, ngx_command_t* cmd, void* conf) {
return NGX_CONF_OK;
}
char* addExporterHeader(ngx_conf_t* cf, ngx_command_t* cmd, void* conf)
{
auto args = (ngx_str_t*)cf->args->elts;
// don't force on users lower case name requirement of gRPC
ngx_strlow(args[1].data, args[1].data, args[1].len);
try {
// validate header here to avoid runtime assert failure in gRPC
auto name = toStrView(args[1]);
if (!Target::validateHeaderName(name)) {
return (char*)"has invalid header name";
}
auto value = toStrView(args[2]);
if (!Target::validateHeaderValue(value)) {
return (char*)"has invalid header value";
}
getMainConf(cf)->headers.emplace_back(name, value);
} catch (const std::exception& e) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "OTel: %s", e.what());
return (char*)NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}
void* createMainConf(ngx_conf_t* cf)
{
auto cln = ngx_pool_cleanup_add(cf->pool, sizeof(MainConf));
@ -769,7 +809,7 @@ void* createMainConf(ngx_conf_t* cf)
char* initMainConf(ngx_conf_t* cf, void* conf)
{
auto mcf = (MainConf*)conf;
auto mcf = getMainConf(cf);
ngx_conf_init_msec_value(mcf->interval, 5000);
ngx_conf_init_size_value(mcf->batchSize, 512);
@ -932,7 +972,7 @@ char* mergeLocationConf(ngx_conf_t* cf, void* parent, void* child)
return NGX_CONF_OK;
}
ngx_http_module_t gHttpModuleCtx = {
ngx_http_module_t ngx_otel_moduleCtx = {
addVariables, /* preconfiguration */
initModule, /* postconfiguration */
@ -948,9 +988,9 @@ ngx_http_module_t gHttpModuleCtx = {
}
ngx_module_t gHttpModule = {
ngx_module_t ngx_otel_module = {
NGX_MODULE_V1,
&gHttpModuleCtx, /* module context */
&ngx_otel_moduleCtx, /* module context */
gCommands, /* module directives */
NGX_HTTP_MODULE, /* module type */
NULL, /* init master */

View file

@ -8,6 +8,27 @@
namespace otel_proto_trace = opentelemetry::proto::collector::trace::v1;
struct Target {
typedef std::vector<std::pair<std::string, std::string>> HeaderVec;
std::string endpoint;
bool ssl;
std::string trustedCert;
HeaderVec headers;
static bool validateHeaderName(StrView name)
{
return grpc_header_key_is_legal(
grpc_slice_from_static_buffer(name.data(), name.size()));
}
static bool validateHeaderValue(StrView value)
{
return grpc_header_nonbin_value_is_legal(
grpc_slice_from_static_buffer(value.data(), value.size()));
}
};
class TraceServiceClient {
public:
typedef otel_proto_trace::ExportTraceServiceRequest Request;
@ -17,18 +38,18 @@ public:
typedef std::function<void (Request, Response, grpc::Status)>
ResponseCb;
TraceServiceClient(const std::string& target, bool ssl,
const std::string& trustedCert)
TraceServiceClient(const Target& target) : headers(target.headers)
{
std::shared_ptr<grpc::ChannelCredentials> creds;
if (ssl) {
if (target.ssl) {
grpc::SslCredentialsOptions options;
options.pem_root_certs = trustedCert;
options.pem_root_certs = target.trustedCert;
creds = grpc::SslCredentials(options);
} else {
creds = grpc::InsecureChannelCredentials();
}
auto channel = grpc::CreateChannel(target, creds);
auto channel = grpc::CreateChannel(target.endpoint, creds);
channel->GetState(true); // trigger 'connecting' state
stub = TraceService::NewStub(channel);
@ -38,6 +59,10 @@ public:
{
std::unique_ptr<ActiveCall> call{new ActiveCall{}};
for (auto& header : headers) {
call->context.AddMetadata(header.first, header.second);
}
call->request = std::move(req);
call->cb = std::move(cb);
@ -107,6 +132,8 @@ private:
ResponseCb cb;
};
Target::HeaderVec headers;
std::unique_ptr<TraceService::Stub> stub;
grpc::CompletionQueue queue;

View file

@ -19,7 +19,7 @@ def pytest_addoption(parser):
parser.addoption("--globals", default="")
def self_signed_cert(test_dir, name):
def self_signed_cert(name):
k = crypto.PKey()
k.generate_key(crypto.TYPE_RSA, 2048)
cert = crypto.X509()
@ -29,11 +29,9 @@ def self_signed_cert(test_dir, name):
cert.gmtime_adj_notAfter(365 * 86400) # 365 days
cert.set_pubkey(k)
cert.sign(k, "sha512")
(test_dir / f"{name}.key").write_text(
crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode("utf-8")
)
(test_dir / f"{name}.crt").write_text(
crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8")
return (
crypto.dump_privatekey(crypto.FILETYPE_PEM, k),
crypto.dump_certificate(crypto.FILETYPE_PEM, cert),
)
@ -66,7 +64,7 @@ def nginx_config(request, pytestconfig, testdir, logger):
@pytest.fixture(scope="module")
def nginx(testdir, pytestconfig, nginx_config, certs, logger, otelcol):
def nginx(testdir, pytestconfig, nginx_config, cert, logger, otelcol):
(testdir / "nginx.conf").write_text(nginx_config)
logger.info("Starting nginx...")
proc = subprocess.Popen(
@ -96,5 +94,8 @@ def nginx(testdir, pytestconfig, nginx_config, certs, logger, otelcol):
@pytest.fixture(scope="module")
def certs(testdir):
self_signed_cert(testdir, "localhost")
def cert(testdir):
key, cert = self_signed_cert("localhost")
(testdir / "localhost.key").write_text(key.decode("utf-8"))
(testdir / "localhost.crt").write_text(cert.decode("utf-8"))
yield (key, cert)

View file

@ -21,14 +21,16 @@ http {
ssl_certificate_key localhost.key;
otel_exporter {
endpoint {{ scheme }}127.0.0.1:14317;
endpoint {{ endpoint or "127.0.0.1:14317" }};
interval {{ interval or "1ms" }};
batch_size 3;
batch_count 3;
{{ exporter_opts }}
}
otel_trace on;
otel_service_name test_service;
{{ resource_attrs }}
server {
listen 127.0.0.1:18443 ssl;
@ -240,7 +242,7 @@ def test_context(client, trace_service, parent, path):
@pytest.mark.parametrize(
"nginx_config",
[({"interval": "200ms", "scheme": "http://"})],
[{"interval": "200ms", "endpoint": "http://127.0.0.1:14317"}],
indirect=True,
)
@pytest.mark.parametrize("batch_count", [1, 3])
@ -257,8 +259,73 @@ def test_batches(client, trace_service, batch_count):
assert len(trace_service.batches) == batch_count
for batch in trace_service.batches:
assert get_attr(batch[0].resource, "service.name") == "test_service"
assert (
get_attr(batch[0].resource, "service.name")
== "unknown_service:nginx"
)
assert len(batch[0].scope_spans[0].spans) == batch_size
time.sleep(0.3) # wait for +1 request to be flushed
trace_service.batches.clear()
@pytest.mark.parametrize(
"nginx_config",
[
{
"resource_attrs": """
otel_service_name "test_service";
otel_resource_attr my.name "my name";
otel_resource_attr my.service "my service";
""",
}
],
indirect=True,
)
def test_custom_resource_attributes(client, trace_service):
assert client.get("http://127.0.0.1:18080/ok").status_code == 200
batch = trace_service.get_batch()
assert get_attr(batch.resource, "service.name") == "test_service"
assert get_attr(batch.resource, "my.name") == "my name"
assert get_attr(batch.resource, "my.service") == "my service"
@pytest.mark.parametrize(
"nginx_config",
[
{
"exporter_opts": """
header X-API-TOKEN api.value;
header Authorization "Basic value";
""",
}
],
indirect=True,
)
@pytest.mark.parametrize("trace_service", ["skip_otelcol"], indirect=True)
def test_exporter_headers(client, trace_service):
assert client.get("http://127.0.0.1:18080/ok").status_code == 200
assert trace_service.get_span().name == "/ok"
headers = dict(trace_service.last_metadata)
assert headers["x-api-token"] == "api.value"
assert headers["authorization"] == "Basic value"
@pytest.mark.parametrize(
"nginx_config",
[
{
"endpoint": "https://localhost:14318",
"exporter_opts": "trusted_certificate localhost.crt;",
}
],
indirect=True,
)
def test_tls_export(client, trace_service):
assert client.get("http://127.0.0.1:18080/ok").status_code == 200
assert trace_service.get_span().name == "/ok"

View file

@ -12,29 +12,42 @@ class TraceService(trace_service_pb2_grpc.TraceServiceServicer):
def Export(self, request, context):
self.batches.append(request.resource_spans)
self.last_metadata = context.invocation_metadata()
return trace_service_pb2.ExportTracePartialSuccess()
def get_span(self):
def get_batch(self):
for _ in range(10):
if len(self.batches):
break
time.sleep(0.001)
assert len(self.batches) == 1
assert len(self.batches[0]) == 1
return self.batches.pop()[0]
assert len(self.batches) == 1, "No spans received"
span = self.batches[0][0].scope_spans[0].spans.pop()
self.batches.clear()
return span
def get_span(self):
batch = self.get_batch()
assert len(batch.scope_spans) == 1
assert len(batch.scope_spans[0].spans) == 1
return batch.scope_spans[0].spans.pop()
@pytest.fixture(scope="module")
def trace_service(pytestconfig, logger):
def trace_service(request, pytestconfig, logger, cert):
server = grpc.server(concurrent.futures.ThreadPoolExecutor())
trace_service = TraceService()
trace_service_pb2_grpc.add_TraceServiceServicer_to_server(
trace_service, server
)
listen_addr = f"127.0.0.1:{24317 if pytestconfig.option.otelcol else 14317}"
trace_service.use_otelcol = (
pytestconfig.option.otelcol
and getattr(request, "param", "") != "skip_otelcol"
)
listen_addr = f"127.0.0.1:{24317 if trace_service.use_otelcol else 14317}"
server.add_insecure_port(listen_addr)
if not trace_service.use_otelcol:
creds = grpc.ssl_server_credentials([cert])
server.add_secure_port("127.0.0.1:14318", creds)
listen_addr += " and 127.0.0.1:14318"
logger.info(f"Starting trace service at {listen_addr}...")
server.start()
yield trace_service
@ -43,18 +56,26 @@ def trace_service(pytestconfig, logger):
@pytest.fixture(scope="module")
def otelcol(pytestconfig, testdir, logger, trace_service):
if pytestconfig.option.otelcol is None:
def otelcol(pytestconfig, testdir, logger, trace_service, cert):
if not trace_service.use_otelcol:
yield
return
(testdir / "otel-config.yaml").write_text(
"""receivers:
f"""receivers:
otlp:
protocols:
grpc:
endpoint: 127.0.0.1:14317
otlp/tls:
protocols:
grpc:
endpoint: 127.0.0.1:14318
tls:
cert_file: {testdir}/localhost.crt
key_file: {testdir}/localhost.key
exporters:
otlp:
endpoint: 127.0.0.1:24317
@ -64,7 +85,7 @@ exporters:
service:
pipelines:
traces:
receivers: [otlp]
receivers: [otlp, otlp/tls]
exporters: [otlp]
telemetry:
metrics: