#include "ngx.hpp" #include "grpc_log.hpp" #include "str_view.hpp" #include "trace_context.hpp" #include "batch_exporter.hpp" #include extern ngx_module_t ngx_otel_module; namespace { struct OtelCtx { TraceContext parent; TraceContext current; }; struct MainConfBase { ngx_str_t endpoint; ngx_msec_t interval; size_t batchSize; size_t batchCount; ngx_str_t serviceName; }; struct MainConf : MainConfBase { std::map resourceAttrs; bool ssl; std::string trustedCert; }; struct SpanAttr { ngx_str_t name; ngx_http_complex_value_t value; }; struct LocationConf { ngx_http_complex_value_t* trace; ngx_uint_t traceContext; ngx_http_complex_value_t* spanName; ngx_array_t spanAttrs; }; 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); namespace Propagation { const ngx_uint_t Extract = 1; const ngx_uint_t Inject = 2; /*const*/ ngx_conf_enum_t Types[] = { { ngx_string("ignore"), 0 }, { ngx_string("extract"), Extract }, { ngx_string("inject"), Inject }, { ngx_string("propagate"), Extract | Inject }, { ngx_null_string, 0 } }; } ngx_command_t gCommands[] = { { ngx_string("otel_exporter"), NGX_HTTP_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS, setExporter }, { ngx_string("otel_resource_attr"), NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE2, addResourceAttr }, { ngx_string("otel_service_name"), NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, NGX_HTTP_MAIN_CONF_OFFSET, offsetof(MainConfBase, serviceName) }, { ngx_string("otel_trace"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_set_complex_value_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(LocationConf, trace) }, { ngx_string("otel_trace_context"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_conf_set_enum_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(LocationConf, traceContext), &Propagation::Types }, { ngx_string("otel_span_name"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_set_complex_value_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(LocationConf, spanName) }, { ngx_string("otel_span_attr"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2, addSpanAttr, NGX_HTTP_LOC_CONF_OFFSET }, ngx_null_command }; ngx_command_t gExporterCommands[] = { { ngx_string("endpoint"), NGX_CONF_TAKE1, ngx_conf_set_str_slot, 0, offsetof(MainConfBase, endpoint) }, { ngx_string("trusted_certificate"), NGX_CONF_TAKE1, setTrustedCertificate }, { ngx_string("interval"), NGX_CONF_TAKE1, ngx_conf_set_msec_slot, 0, offsetof(MainConfBase, interval) }, { ngx_string("batch_size"), NGX_CONF_TAKE1, ngx_conf_set_size_slot, 0, offsetof(MainConfBase, batchSize) }, { ngx_string("batch_count"), NGX_CONF_TAKE1, ngx_conf_set_size_slot, 0, offsetof(MainConfBase, batchCount) }, ngx_null_command }; std::unique_ptr gExporter; StrView toStrView(ngx_str_t str) { return StrView((char*)str.data, str.len); } ngx_str_t toNgxStr(StrView str) { return ngx_str_t{str.size(), (u_char*)str.data()}; } bool iremovePrefix(ngx_str_t* str, StrView p) { if (str->len >= p.size() && ngx_strncasecmp(str->data, (u_char*)p.data(), p.size()) == 0) { str->data += p.size(); str->len -= p.size(); return true; } return false; } MainConf* getMainConf(ngx_conf_t* cf) { return static_cast( (MainConfBase*)ngx_http_conf_get_module_main_conf(cf, ngx_otel_module)); } MainConf* getMainConf(ngx_cycle_t* cycle) { return static_cast( (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, ngx_otel_module); } void cleanupOtelCtx(void* data) { } OtelCtx* getOtelCtx(ngx_http_request_t* r) { 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)) { for (auto cln = r->pool->cleanup; cln; cln = cln->next) { if (cln->handler == cleanupOtelCtx) { ctx = (OtelCtx*)cln->data; ngx_http_set_ctx(r, ctx, ngx_otel_module); break; } } } return ctx; } OtelCtx* createOtelCtx(ngx_http_request_t* r) { static_assert(std::is_trivially_destructible::value, ""); auto storage = ngx_pool_cleanup_add(r->pool, sizeof(OtelCtx)); if (storage == NULL) { return NULL; } storage->handler = cleanupOtelCtx; auto ctx = new (storage->data) OtelCtx{}; ngx_http_set_ctx(r, ctx, ngx_otel_module); return ctx; } ngx_table_elt_t* findHeader(ngx_list_t* list, ngx_uint_t hash, StrView key) { auto part = &list->part; auto elts = (ngx_table_elt_t*)part->elts; for (ngx_uint_t i = 0; /* void */; i++) { if (i >= part->nelts) { if (part->next == NULL) { break; } part = part->next; elts = (ngx_table_elt_t*)part->elts; i = 0; } if (elts[i].hash != hash || elts[i].key.len != key.size() || ngx_memcmp(elts[i].lowcase_key, key.data(), key.size()) != 0) { continue; } return &elts[i]; } return NULL; } StrView getHeader(ngx_http_request_t* r, StrView name) { auto hash = ngx_hash_key((u_char*)name.data(), name.size()); auto header = findHeader(&r->headers_in.headers, hash, name); return header ? toStrView(header->value) : StrView{}; } ngx_int_t updateRequestHeader(ngx_http_request_t* r, ngx_table_elt_t* header) { auto cmcf = (ngx_http_core_main_conf_t*) ngx_http_get_module_main_conf(r, ngx_http_core_module); auto hh = (ngx_http_header_t*)ngx_hash_find(&cmcf->headers_in_hash, header->hash, header->lowcase_key, header->key.len); return hh ? hh->handler(r, header, hh->offset) : NGX_OK; } ngx_int_t setHeader(ngx_http_request_t* r, StrView name, StrView value) { auto hash = ngx_hash_key((u_char*)name.data(), name.size()); auto header = findHeader(&r->headers_in.headers, hash, name); if (header == NULL) { if (value.empty()) { return NGX_OK; } auto headers = &r->headers_in.headers; if (!headers->pool && ngx_list_init(headers, r->pool, 2, sizeof(ngx_table_elt_t)) != NGX_OK) { return NGX_ERROR; } header = (ngx_table_elt_t*)ngx_list_push(headers); if (header == NULL) { return NGX_ERROR; } header->hash = hash; header->key = toNgxStr(name); header->lowcase_key = header->key.data; header->next = NULL; } header->value = toNgxStr(value); return updateRequestHeader(r, header); } TraceContext extract(ngx_http_request_t* r) { auto parent = getHeader(r, "traceparent"); auto state = getHeader(r, "tracestate"); return TraceContext::parse(parent, state); } ngx_int_t inject(ngx_http_request_t* r, const TraceContext& tc) { auto buf = (char*)ngx_pnalloc(r->pool, TraceContext::Size); if (buf == NULL) { return NGX_ERROR; } TraceContext::serialize(tc, buf); auto rc = setHeader(r, "traceparent", {buf, TraceContext::Size}); if (rc != NGX_OK) { return rc; } return setHeader(r, "tracestate", tc.state); } OtelCtx* ensureOtelCtx(ngx_http_request_t* r) { auto ctx = getOtelCtx(r); if (ctx) { return ctx; } ctx = createOtelCtx(r); if (!ctx) { return NULL; } auto lcf = getLocationConf(r); if (lcf->traceContext & Propagation::Extract) { ctx->parent = extract(r); } ctx->current = TraceContext::generate(false, ctx->parent); return ctx; } ngx_int_t onRequestStart(ngx_http_request_t* r) { // don't let internal redirects to override sampling decision if (r->internal) { return NGX_DECLINED; } bool sampled = false; auto lcf = getLocationConf(r); if (lcf->trace != NULL) { ngx_str_t trace; if (ngx_http_complex_value(r, lcf->trace, &trace) != NGX_OK) { return NGX_ERROR; } sampled = toStrView(trace) == "on" || toStrView(trace) == "1"; } if (!lcf->traceContext && !sampled) { return NGX_DECLINED; } auto ctx = ensureOtelCtx(r); if (!ctx) { return NGX_ERROR; } ctx->current.sampled = sampled; ngx_int_t rc = NGX_OK; if (lcf->traceContext & Propagation::Inject) { rc = inject(r, ctx->current); } return rc == NGX_OK ? NGX_DECLINED : rc; } StrView getServerName(ngx_http_request_t* r) { auto cscf = (ngx_http_core_srv_conf_t*) ngx_http_get_module_srv_conf(r, ngx_http_core_module); auto name = cscf->server_name; if (name.len == 0) { name = r->headers_in.server; } return toStrView(name); } void addDefaultAttrs(BatchExporter::Span& span, ngx_http_request_t* r) { // based on trace semantic conventions for HTTP from 1.16.0 OTel spec span.add("http.method", toStrView(r->method_name)); span.add("http.target", toStrView(r->unparsed_uri)); auto clcf = (ngx_http_core_loc_conf_t*) ngx_http_get_module_loc_conf(r, ngx_http_core_module); if (clcf->name.len) { span.add("http.route", toStrView(clcf->name)); } span.add("http.scheme", r->connection->ssl ? "https" : "http"); auto protocol = toStrView(r->http_protocol); if (protocol.size() > 5) { // "HTTP/" span.add("http.flavor", protocol.substr(5)); } if (r->headers_in.user_agent) { span.add("http.user_agent", toStrView(r->headers_in.user_agent->value)); } auto received = r->headers_in.content_length_n; span.add("http.request_content_length", received > 0 ? received : 0); auto sent = r->connection->sent - (off_t)r->header_size; span.add("http.response_content_length", sent > 0 ? sent : 0); auto status = r->err_status ? r->err_status : r->headers_out.status; if (status) { span.add("http.status_code", status); if (status >= 500) { span.setError(); } } span.add("net.host.name", getServerName(r)); if (ngx_connection_local_sockaddr(r->connection, NULL, 0) == NGX_OK) { auto port = ngx_inet_get_port(r->connection->local_sockaddr); auto defaultPort = r->connection->ssl ? 443 : 80; if (port != defaultPort) { span.add("net.host.port", port); } } span.add("net.sock.peer.addr", toStrView(r->connection->addr_text)); span.add("net.sock.peer.port", ngx_inet_get_port(r->connection->sockaddr)); } StrView getSpanName(ngx_http_request_t* r) { auto lcf = getLocationConf(r); if (lcf->spanName) { ngx_str_t result; if (ngx_http_complex_value(r, lcf->spanName, &result) != NGX_OK) { throw std::runtime_error("failed to compute complex value"); } return toStrView(result); } else { auto clcf = (ngx_http_core_loc_conf_t*) ngx_http_get_module_loc_conf(r, ngx_http_core_module); return toStrView(clcf->name); } } void addCustomAttrs(BatchExporter::Span& span, ngx_http_request_t* r) { auto lcf = getLocationConf(r); auto attrs = (SpanAttr*)lcf->spanAttrs.elts; for (ngx_uint_t i = 0; i < lcf->spanAttrs.nelts; i++) { ngx_str_t value; if (ngx_http_complex_value(r, &attrs[i].value, &value) != NGX_OK) { throw std::runtime_error("failed to compute complex value"); } StrView name = toStrView(attrs[i].name); if (startsWith(name, "http.request.header.") || startsWith(name, "http.response.header.")) { //TODO: remove this once headers are supported natively span.addArray(name, toStrView(value)); } else { span.add(name, toStrView(value)); } } } ngx_int_t onRequestEnd(ngx_http_request_t* r) { auto ctx = getOtelCtx(r); if (!ctx || !ctx->current.sampled) { return NGX_DECLINED; } auto now = ngx_timeofday(); auto toNanoSec = [](time_t sec, ngx_msec_t msec) -> uint64_t { return (sec * 1000 + msec) * 1000000; }; try { BatchExporter::SpanInfo info{ getSpanName(r), ctx->current, ctx->parent.spanId, toNanoSec(r->start_sec, r->start_msec), toNanoSec(now->sec, now->msec)}; bool ok = gExporter->add(info, [r](BatchExporter::Span& span) { addDefaultAttrs(span, r); addCustomAttrs(span, r); }); if (!ok) { static size_t dropped = 0; static time_t lastLog = 0; ++dropped; if (lastLog != ngx_time()) { lastLog = ngx_time(); ngx_log_error(NGX_LOG_NOTICE, r->connection->log, 0, "OTel dropped records: %uz", dropped); } } } catch (const std::exception& e) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "OTel failed to add span: %s", e.what()); return NGX_ERROR; } return NGX_DECLINED; } ngx_int_t initModule(ngx_conf_t* cf) { auto cmcf = (ngx_http_core_main_conf_t*)ngx_http_conf_get_module_main_conf( cf, ngx_http_core_module); auto h = (ngx_http_handler_pt*)ngx_array_push( &cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers); if (h == NULL) { return NGX_ERROR; } *h = onRequestStart; h = (ngx_http_handler_pt*)ngx_array_push( &cmcf->phases[NGX_HTTP_LOG_PHASE].handlers); if (h == NULL) { return NGX_ERROR; } *h = onRequestEnd; initGrpcLog(); return NGX_OK; } ngx_int_t initWorkerProcess(ngx_cycle_t* cycle) { auto mcf = getMainConf(cycle); // no 'http' or 'otel_exporter' blocks if (mcf == NULL || mcf->endpoint.len == 0) { return NGX_OK; } try { gExporter.reset(new BatchExporter( toStrView(mcf->endpoint), mcf->ssl, mcf->trustedCert, mcf->batchSize, mcf->batchCount, mcf->resourceAttrs)); } catch (const std::exception& e) { ngx_log_error(NGX_LOG_CRIT, cycle->log, 0, "OTel worker init error: %s", e.what()); return NGX_ERROR; } static ngx_connection_t dummy; static ngx_event_t flushEvent; flushEvent.data = &dummy; flushEvent.log = cycle->log; flushEvent.cancelable = 1; flushEvent.handler = [](ngx_event_t* ev) { try { gExporter->flush(); } catch (const std::exception& e) { ngx_log_error(NGX_LOG_CRIT, ev->log, 0, "OTel flush error: %s", e.what()); } auto mcf = getMainConf((ngx_cycle_t*)ngx_cycle); ngx_add_timer(ev, mcf->interval); }; ngx_add_timer(&flushEvent, mcf->interval); return NGX_OK; } void exitWorkerProcess(ngx_cycle_t* cycle) { if (!gExporter) { return; } try { gExporter->flush(); } catch (const std::exception& e) { ngx_log_error(NGX_LOG_CRIT, cycle->log, 0, "OTel flush error: %s", e.what()); } gExporter.reset(); } char* setExporter(ngx_conf_t* cf, ngx_command_t* cmd, void* conf) { auto mcf = getMainConf(cf); if (mcf->endpoint.len) { return (char*)"is duplicate"; } auto cfCopy = *cf; cfCopy.handler = [](ngx_conf_t* cf, ngx_command_t*, void*) { auto name = (ngx_str_t*)cf->args->elts; for (auto cmd = gExporterCommands; cmd->name.len; cmd++) { if (ngx_strcmp(name->data, cmd->name.data) != 0) { continue; } if (cf->args->nelts != 2) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid number of arguments in \"%V\" " "directive of \"otel_exporter\"", name); return (char*)NGX_CONF_ERROR; } auto rv = cmd->set(cf, cmd, cf->handler_conf); if (rv == NGX_CONF_OK) { return rv; } if (rv != NGX_CONF_ERROR) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "\"%V\" directive of \"otel_exporter\" %s", name, rv); } return (char*)NGX_CONF_ERROR; } ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "unknown directive \"%V\" in \"otel_exporter\"", name); return (char*)NGX_CONF_ERROR; }; cfCopy.handler_conf = mcf; auto rv = ngx_conf_parse(&cfCopy, NULL); if (rv != NGX_CONF_OK) { return rv; } if (iremovePrefix(&mcf->endpoint, "https://")) { mcf->ssl = true; } else { iremovePrefix(&mcf->endpoint, "http://"); } if (mcf->endpoint.len == 0) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "\"otel_exporter\" requires \"endpoint\""); return (char*)NGX_CONF_ERROR; } return NGX_CONF_OK; } char* addResourceAttr(ngx_conf_t* cf, ngx_command_t* cmd, void* conf) { auto mcf = getMainConf(cf); try { auto args = (ngx_str_t*)cf->args->elts; mcf->resourceAttrs[toStrView(args[1])] = toStrView(args[2]); } 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; } 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); if (ngx_get_full_name(cf->pool, &cf->cycle->conf_prefix, &path) != NGX_OK) { return (char*)NGX_CONF_ERROR; } try { std::ifstream file{(const char*)path.data, std::ios::binary}; if (!file.is_open()) { ngx_conf_log_error(NGX_LOG_EMERG, cf, ngx_errno, "failed to open \"%V\"", &path); 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.seekg(0); file.read(&mcf->trustedCert[0], mcf->trustedCert.size()); } catch (const std::exception& e) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "failed to read \"%V\": %s", &path, 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)); if (cln == NULL) { return NULL; } MainConf* mcf; try { mcf = new (cln->data) MainConf{}; } catch (const std::exception& e) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "OTel: %s", e.what()); return NULL; } cln->handler = [](void* data) { ((MainConf*)data)->~MainConf(); }; mcf->interval = NGX_CONF_UNSET_MSEC; mcf->batchSize = NGX_CONF_UNSET_SIZE; mcf->batchCount = NGX_CONF_UNSET_SIZE; return static_cast(mcf); } char* initMainConf(ngx_conf_t* cf, void* conf) { auto mcf = (MainConf*)conf; ngx_conf_init_msec_value(mcf->interval, 5000); ngx_conf_init_size_value(mcf->batchSize, 512); ngx_conf_init_size_value(mcf->batchCount, 4); try { if (mcf->serviceName.data == NULL) { mcf->resourceAttrs.emplace("service.name", "unknown_service:nginx"); } else { mcf->resourceAttrs["service.name"] = toStrView(mcf->serviceName); } } 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; } char* addSpanAttr(ngx_conf_t* cf, ngx_command_t* cmd, void* conf) { auto lcf = (LocationConf*)conf; if (lcf->spanAttrs.elts == NULL && ngx_array_init(&lcf->spanAttrs, cf->pool, 4, sizeof(SpanAttr)) != NGX_OK) { return (char*)NGX_CONF_ERROR; } auto attr = (SpanAttr*)ngx_array_push(&lcf->spanAttrs); if (attr == NULL) { return (char*)NGX_CONF_ERROR; } auto args = (ngx_str_t*)cf->args->elts; attr->name = args[1]; ngx_http_compile_complex_value_t ccv = { cf, &args[2], &attr->value }; if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { return (char*)NGX_CONF_ERROR; } return NGX_CONF_OK; } template ngx_int_t hexIdVar(ngx_http_request_t* r, ngx_http_variable_value_t* v, uintptr_t data) { auto ctx = ensureOtelCtx(r); if (!ctx) { return NGX_ERROR; } auto id = (Id*)((char*)ctx + data); if (id->IsValid()) { auto size = id->Id().size() * 2; auto buf = (char*)ngx_pnalloc(r->pool, size); if (buf == NULL) { return NGX_ERROR; } id->ToLowerBase16({buf, size}); v->len = size; v->valid = 1; v->no_cacheable = 0; v->not_found = 0; v->data = (u_char*)buf; } else { v->not_found = 1; } return NGX_OK; } ngx_int_t parentSampledVar(ngx_http_request_t* r, ngx_http_variable_value_t* v, uintptr_t data) { auto ctx = ensureOtelCtx(r); if (!ctx) { return NGX_ERROR; } v->len = 1; v->valid = 1; v->no_cacheable = 0; v->not_found = 0; v->data = (u_char*)(ctx->parent.sampled ? "1" : "0"); return NGX_OK; } ngx_int_t addVariables(ngx_conf_t* cf) { using namespace opentelemetry::trace; ngx_http_variable_t vars[] = { { ngx_string("otel_trace_id"), NULL, hexIdVar, offsetof(OtelCtx, current.traceId) }, { ngx_string("otel_span_id"), NULL, hexIdVar, offsetof(OtelCtx, current.spanId) }, { ngx_string("otel_parent_id"), NULL, hexIdVar, offsetof(OtelCtx, parent.spanId) }, { ngx_string("otel_parent_sampled"), NULL, parentSampledVar } }; for (auto& v : vars) { auto var = ngx_http_add_variable(cf, &v.name, 0); if (var == NULL) { return NGX_ERROR; } var->get_handler = v.get_handler; var->data = v.data; } return NGX_OK; } void* createLocationConf(ngx_conf_t* cf) { auto conf = (LocationConf*)ngx_pcalloc(cf->pool, sizeof(LocationConf)); if (conf == NULL) { return NULL; } conf->trace = (ngx_http_complex_value_t*)NGX_CONF_UNSET_PTR; conf->traceContext = NGX_CONF_UNSET_UINT; conf->spanName = (ngx_http_complex_value_t*)NGX_CONF_UNSET_PTR; return conf; } char* mergeLocationConf(ngx_conf_t* cf, void* parent, void* child) { auto prev = (LocationConf*)parent; auto conf = (LocationConf*)child; ngx_conf_merge_ptr_value(conf->trace, prev->trace, NULL); ngx_conf_merge_uint_value(conf->traceContext, prev->traceContext, 0); ngx_conf_merge_ptr_value(conf->spanName, prev->spanName, NULL); if (conf->spanAttrs.elts == NULL) { conf->spanAttrs = prev->spanAttrs; } auto mcf = getMainConf(cf); if (mcf->endpoint.len == 0 && conf->trace) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "\"otel_exporter\" block is missing"); return (char*)NGX_CONF_ERROR; } return NGX_CONF_OK; } ngx_http_module_t ngx_otel_moduleCtx = { addVariables, /* preconfiguration */ initModule, /* postconfiguration */ createMainConf, /* create main configuration */ initMainConf, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ createLocationConf, /* create location configuration */ mergeLocationConf /* merge location configuration */ }; } ngx_module_t ngx_otel_module = { NGX_MODULE_V1, &ngx_otel_moduleCtx, /* module context */ gCommands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ initWorkerProcess, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ exitWorkerProcess, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING };