Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@

# scratch directory for preparing releases
/.release/

# Python
__pycache__/
4 changes: 2 additions & 2 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,8 @@ If there is no location associated with the current request, then

### `datadog_proxy_directive`
`$datadog_proxy_directive` expands to the name of the configuration directive
used to proxy the current request, i.e. one of `proxy_pass`, `grpc_pass`, or
`fastcgi_pass`.
used to proxy the current request, i.e. one of `proxy_pass`, `grpc_pass`,
`fastcgi_pass`, or `uwsgi_pass`.

If the request was not configured by one of those directives, then
`$datadog_proxy_directive` expands to "`location`".
Expand Down
1 change: 1 addition & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ services:
- `http` runs a NodeJS HTTP server that listens on port 8080.
- `fastcgi` runs a NodeJS FastCGI server that listens on port 8080.
- `grpc` runs a NodeJS gRPC server that listens on port 1337.
- `uwsgi` runs a Python uWSGI server that listens on port 8080.
- `client` contains the command line tools `curl` and `grpcurl`.

Because client command line tools are available within the `client` service,
Expand Down
13 changes: 13 additions & 0 deletions example/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ services:
- http
- fastcgi
- grpc
- uwsgi
- agent

# `agent` is the Datadog Agent to which traces are sent.
Expand Down Expand Up @@ -69,6 +70,18 @@ services:
depends_on:
- agent

# `uwsgi` is a uWSGI server that is reverse proxied by `nginx`.
uwsgi:
build:
context: ./services/uwsgi
dockerfile: ./Dockerfile
environment:
- DD_ENV=prod
- DD_AGENT_HOST=agent
- DD_SERVICE=uwsgi
depends_on:
- agent

# `client` is a container that exists solely so that the host can
# `docker-compose exec` into it to run command line tools such as `curl` and
# `grpcurl`. This way, the host's network doesn't need access to the network
Expand Down
5 changes: 5 additions & 0 deletions example/services/nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ http {
location /fastcgi {
fastcgi_pass fastcgi:8080;
}

location /uwsgi {
include uwsgi_params;
uwsgi_pass uwsgi:8080;
}
}

server {
Expand Down
13 changes: 13 additions & 0 deletions example/services/uwsgi/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM alpine:3.15

RUN mkdir /opt/app
WORKDIR /opt/app

RUN apk update && apk add python3 py3-pip python3-dev build-base linux-headers pcre-dev
RUN pip3 install --no-cache-dir uwsgi flask ddtrace

COPY ./uwsgi.ini /opt/app/uwsgi.ini
COPY ./wsgi.py /opt/app/wsgi.py
COPY ./app.py /opt/app/app.py

CMD ["uwsgi", "--ini", "uwsgi.ini"]
14 changes: 14 additions & 0 deletions example/services/uwsgi/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/", defaults={"path": ""})
@app.route("/<string:path>")
@app.route("/<path:path>")
def main(path):
response = {
"service": "uwsgi",
"headers": dict(request.headers)
}
print(response)
return jsonify(response)
11 changes: 11 additions & 0 deletions example/services/uwsgi/uwsgi.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[uwsgi]
module = wsgi:app
master = true
socket = :8080

;; dd-trace-py is used to trace the Flask application.
;; It requires these options to supports uWSGI.
;; See <https://ddtrace.readthedocs.io/en/stable/advanced_usage.html#uwsgi>.
enable-threads = 1
lazy-apps = 1
import=ddtrace.bootstrap.sitecustomize
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a couple of minutes to figure this out, because the spot where ddtrace is installed is quite subtle.
Maybe some additional comments would be helpful

Copy link
Contributor Author

@dmehala dmehala Sep 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added more informations. As a non native english speaker, any suggestion will be appreciated :) Resolved in d475f16

4 changes: 4 additions & 0 deletions example/services/uwsgi/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from app import app

if __name__ == "__main__":
app.run()
2 changes: 1 addition & 1 deletion module/config
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ ngx_module_name="$ngx_addon_name"
# configuration directives we override. This way, our module can define
# handlers for those directives that do some processing and then forward
# to the "real" handler in the other module.
ngx_module_order="$ngx_addon_name ngx_http_log_module ngx_http_fastcgi_module ngx_http_grpc_module ngx_http_proxy_module ngx_http_api_module"
ngx_module_order="$ngx_addon_name ngx_http_log_module ngx_http_fastcgi_module ngx_http_grpc_module ngx_http_proxy_module ngx_http_api_module ngx_http_uwsgi_module"

. auto/module
51 changes: 51 additions & 0 deletions src/datadog_directive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,57 @@ char *hijack_grpc_pass(ngx_conf_t *cf, ngx_command_t *command, void *conf) noexc
return hijack_pass_directive(&propagate_grpc_datadog_context, cf, command, conf);
}

char *propagate_uwsgi_datadog_context(ngx_conf_t *cf, ngx_command_t *command, void * /*conf*/) noexcept try {
auto main_conf = static_cast<datadog_main_conf_t *>(
ngx_http_conf_get_module_main_conf(cf, ngx_http_datadog_module));

// The only way that `main_conf` could be `nullptr` is if there's no `http`
// block in the nginx configuration. In that case, this function would never
// get called, because it's called only from configuration directives that
// live inside the `http` block.
assert(main_conf != nullptr);

if (!main_conf->are_propagation_styles_locked) {
if (auto rcode = lock_propagation_styles(command, cf)) {
return rcode;
}
}
// For each propagation header (from `span_context_keys`), add a
// "uwsgi_param ...;" directive to the configuration, and then process the
// injected directive by calling `datadog_conf_handler`.
const auto &keys = main_conf->span_context_keys;

auto old_args = cf->args;

ngx_str_t args[] = {ngx_string("uwsgi_param"), ngx_str_t(), ngx_str_t(),
ngx_string("if_not_empty")};
ngx_array_t args_array;
args_array.elts = static_cast<void *>(&args);
args_array.nelts = 4;

cf->args = &args_array;
const auto guard = defer([&]() { cf->args = old_args; });

for (const std::string_view key : keys) {
// NOTE(@dmehala): uWSGI uses the same key header convention as fastcgi
args[1] = make_fastcgi_span_context_key(cf->pool, key);
args[2] = make_propagation_header_variable(cf->pool, key);
auto rcode = datadog_conf_handler({.conf = cf, .skip_this_module = true});
if (rcode != NGX_OK) {
return static_cast<char *>(NGX_CONF_ERROR);
}
}

return static_cast<char *>(NGX_CONF_OK);
} catch (const std::exception &e) {
ngx_log_error(NGX_LOG_ERR, cf->log, 0, "propagate_uwsgi_datadog_context failed: %s", e.what());
return static_cast<char *>(NGX_CONF_ERROR);
}

char *hijack_uwsgi_pass(ngx_conf_t *cf, ngx_command_t *command, void *conf) noexcept {
return hijack_pass_directive(&propagate_uwsgi_datadog_context, cf, command, conf);
}

char *hijack_access_log(ngx_conf_t *cf, ngx_command_t *command, void *conf) noexcept try {
// In case we need to change the `access_log` command's format to a
// Datadog-specific default, first make sure that those formats are defined.
Expand Down
2 changes: 2 additions & 0 deletions src/datadog_directive.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ char *propagate_grpc_datadog_context(ngx_conf_t *cf, ngx_command_t *command, voi

char *hijack_grpc_pass(ngx_conf_t *cf, ngx_command_t *command, void *conf) noexcept;

char *hijack_uwsgi_pass(ngx_conf_t *cf, ngx_command_t *command, void *conf) noexcept;

char *hijack_access_log(ngx_conf_t *cf, ngx_command_t *command, void *conf) noexcept;

char *add_datadog_tag(ngx_conf_t *cf, ngx_array_t *tags, ngx_str_t key, ngx_str_t value) noexcept;
Expand Down
7 changes: 7 additions & 0 deletions src/ngx_http_datadog_module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ static ngx_command_t datadog_commands[] = {
0,
nullptr},

{ ngx_string("uwsgi_pass"),
anywhere_but_main | NGX_CONF_TAKE1,
hijack_uwsgi_pass,
NGX_HTTP_LOC_CONF_OFFSET,
0,
nullptr},

// The configuration of this directive was copied from
// ../nginx/src/http/modules/ngx_http_log_module.c.
{ ngx_string("access_log"),
Expand Down
16 changes: 16 additions & 0 deletions test/cases/auto_propagation/conf/uwsgi_auto.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load_module modules/ngx_http_datadog_module.so;

events {
worker_connections 1024;
}

http {
server {
listen 80;

location / {
include /etc/nginx/uwsgi_params;
uwsgi_pass uwsgi:8080;
}
}
}
17 changes: 17 additions & 0 deletions test/cases/auto_propagation/conf/uwsgi_disabled_at_http.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load_module modules/ngx_http_datadog_module.so;

events {
worker_connections 1024;
}

http {
datadog_disable;
server {
listen 80;

location / {
include /etc/nginx/uwsgi_params;
uwsgi_pass uwsgi:8080;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load_module modules/ngx_http_datadog_module.so;

events {
worker_connections 1024;
}

http {
server {
listen 80;

location / {
datadog_disable;
include /etc/nginx/uwsgi_params;
uwsgi_pass uwsgi:8080;
}
}
}
17 changes: 17 additions & 0 deletions test/cases/auto_propagation/conf/uwsgi_disabled_at_server.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load_module modules/ngx_http_datadog_module.so;

events {
worker_connections 1024;
}

http {
server {
listen 80;
datadog_disable;

location / {
include /etc/nginx/uwsgi_params;
uwsgi_pass uwsgi:8080;
}
}
}
15 changes: 15 additions & 0 deletions test/cases/auto_propagation/conf/uwsgi_without_module.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

events {
worker_connections 1024;
}

http {
server {
listen 80;

location / {
include /etc/nginx/uwsgi_params;
uwsgi_pass uwsgi:8080;
}
}
}
45 changes: 45 additions & 0 deletions test/cases/auto_propagation/test_uwsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from .. import case

import json
from pathlib import Path


class TestUWSGI(case.TestCase):

def test_auto_propagation(self):
return self.run_test('./conf/uwsgi_auto.conf', should_propagate=True)

def test_disabled_at_location(self):
return self.run_test('./conf/uwsgi_disabled_at_location.conf',
should_propagate=False)

def test_disabled_at_server(self):
return self.run_test('./conf/uwsgi_disabled_at_server.conf',
should_propagate=False)

def test_disabled_at_http(self):
return self.run_test('./conf/uwsgi_disabled_at_http.conf',
should_propagate=False)

def test_without_module(self):
return self.run_test('./conf/uwsgi_without_module.conf',
should_propagate=False)

def run_test(self, conf_relative_path, should_propagate):
conf_path = Path(__file__).parent / conf_relative_path
conf_text = conf_path.read_text()
status, log_lines = self.orch.nginx_replace_config(
conf_text, conf_path.name)
self.assertEqual(status, 0, log_lines)

status, body = self.orch.send_nginx_http_request('/')
self.assertEqual(status, 200)
response = json.loads(body)
self.assertEqual(response['service'], 'uwsgi')
headers = response['headers']
if should_propagate:
self.assertIn('X-Datadog-Sampling-Priority', headers)
priority = headers['X-Datadog-Sampling-Priority']
priority = int(priority)
else:
self.assertNotIn('X-Datadog-Sampling-Priority', headers)
17 changes: 17 additions & 0 deletions test/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ services:
- fastcgi
- grpc
- agent
- uwsgi

# `agent` is a mock trace agent. It listens on port 8126, accepts msgpack,
# decodes the resulting traces, and prints them to standard output as JSON.
Expand Down Expand Up @@ -63,6 +64,22 @@ services:
depends_on:
- agent

# `uwsgi` is a uWSGI server that is reverse proxied by `nginx`. It
# listens on port 8080 and responds with a JSON object containing the name of
# the service ("uwsgi") and the request's headers. This way, tests can see
# which trace context, if any, was propagated to the reverse proxied server.
uwsgi:
image: nginx-datadog-test-services-uwsgi
build:
context: ./services/uwsgi
dockerfile: ./Dockerfile
environment:
- DD_ENV=prod
- DD_AGENT_HOST=agent
- DD_SERVICE=uwsgi
depends_on:
- agent

# `grpc` is a gRPC server that is reverse proxied by `nginx`. It
# listens on port 1337 and responds with a message containing the metadata
# sent with the request. This way, tests can see
Expand Down
13 changes: 13 additions & 0 deletions test/services/uwsgi/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM alpine:3.15

RUN mkdir /opt/app
WORKDIR /opt/app

RUN apk update && apk add python3 py3-pip python3-dev build-base linux-headers pcre-dev
RUN pip3 install --no-cache-dir uwsgi flask ddtrace

COPY ./uwsgi.ini /opt/app/uwsgi.ini
COPY ./wsgi.py /opt/app/wsgi.py
COPY ./app.py /opt/app/app.py

CMD ["uwsgi", "--ini", "uwsgi.ini"]
12 changes: 12 additions & 0 deletions test/services/uwsgi/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/")
def main():
response = {
"service": "uwsgi",
"headers": dict(request.headers)
}
print(response)
return jsonify(response)
Loading