Repository: DataDog/dd-trace-py

Issue:
Add RQ support
I was wondering if there was any work being done to support [RQ](https://github.com/rq/rq) so that job information is submitted to Datadog.  

If not, then I'm considering implementing the feature myself (but I think I might need a little guidence).



Discussion:
hey @luongthomas, I actually ran into someone else recently who was using this.

We do not currently have RQ on our roadmap but if you were interested in writing an integration we'd be happy to try and help out!
I definitely don't know enough about `rq`, but from a quick glance at the code, it seems like the majority of what we'd want to trace is in `rq/job.py` in the `Job` class.
would like to add to this request. we are moving from celery to rq and would be very grateful for this integration.
Hi everyone, wanted to share something we've come up with around tracing RQ jobs...

```python
from ddtrace.contrib.django.conf import settings
from rq.job import Job

tracer = settings.TRACER  # build a tracer however you'd like, we configure w/ Django so YMMV

JOB_SERVICE_NAME = 'rq-worker'

class DatadogTracedJob(Job):
    def perform(self):
        with tracer.trace(self.func_name, service=JOB_SERVICE_NAME):
            super().perform()
```

Then you should configure RQ to use this custom Job class.

This isn't perfect, however. We're noticing a small portion of spans come through. What I think is happening is that because RQ uses a forking model in which the code above runs in the "horse" process (their words, not mine :) that exits quickly after the job finishes, the internal queue that holds spans and submits to the agent on an interval is destroyed before this submission. We dealt w/ this when [integrating with Elastic's APM](https://discuss.elastic.co/t/python-agent-only-recording-some-background-tasks/149707) and it just smells like the same underlying issue.

@brettlangdon  is there a way to force this queue to flush? If so, a good place to do that would be just after the `with tracer.trace()`context block so we'd know that the spans from the trace have been enqueued but before the job finishes (and the process dies). Alternatively, and this smells to me, would be to `sleep()` for the interval the writer periodically writes so we could ensure a flush to the agent would be triggered before the job finishes.

Obviously this isn't _great_ but depending on average background job length it may or may not have a detrimental impact on overall job processing capacity of your system.
    
hey @sburns great find, and yeah from the sounds of it, I'd guess that is the issue as well (we have the same problem with Resque for Ruby).

You can force a flush with the following:

```python
from ddtrace import tracer
tracer.writer.stop()
tracer.writer.flush()
```
> (we have the same problem with Resque for Ruby)

funny, because the design of RQ greatly inherits from resque :)
On 0.29.0, what should happen after the `tracer.trace` context block should be:

```python
tracer.writer.write()
tracer.writer._worker.stop()
tracer.writer._worker.flush_queue()
```
@sburns you are right, sorry, I was looking at `master` and thought those changes were already released.

Please be sure upon upgrading to `0.30.0` what I posted should work.

Resolving patch:
diff --git a/ddtrace/contrib/rq/__init__.py b/ddtrace/contrib/rq/__init__.py
new file mode 100644
--- /dev/null
+++ b/ddtrace/contrib/rq/__init__.py
@@ -0,0 +1,251 @@
+"""
+The RQ__ integration will trace your jobs.
+
+
+Usage
+~~~~~
+
+The rq integration is enabled automatically when using
+:ref:`ddtrace-run<ddtracerun>` or :ref:`patch_all()<patch_all>`.
+
+Or use :ref:`patch()<patch>` to manually enable the integration::
+
+    from ddtrace import patch
+    patch(rq=True)
+
+
+Worker Usage
+~~~~~~~~~~~~
+
+``ddtrace-run`` can be used to easily trace your workers::
+
+    DD_SERVICE=myworker ddtrace-run rq worker
+
+
+
+Instance Configuration
+~~~~~~~~~~~~~~~~~~~~~~
+
+To override the service name for a queue::
+
+    from ddtrace import Pin
+
+    connection = redis.Redis()
+    queue = rq.Queue(connection=connection)
+    Pin.override(queue, service="custom_queue_service")
+
+
+To override the service name for a particular worker::
+
+    worker = rq.SimpleWorker([queue], connection=queue.connection)
+    Pin.override(worker, service="custom_worker_service")
+
+
+Global Configuration
+~~~~~~~~~~~~~~~~~~~~
+
+.. py:data:: ddtrace.config.rq['distributed_tracing_enabled']
+.. py:data:: ddtrace.config.rq_worker['distributed_tracing_enabled']
+
+   If ``True`` the integration will connect the traces sent between the enqueuer
+   and the RQ worker.
+
+   This option can also be set with the ``DD_RQ_DISTRIBUTED_TRACING_ENABLED``
+   environment variable on either the enqueuer or worker applications.
+
+   Default: ``True``
+
+.. py:data:: ddtrace.config.rq['service']
+
+   The service name reported by default for RQ spans from the app.
+
+   This option can also be set with the ``DD_SERVICE`` or ``DD_RQ_SERVICE``
+   environment variables.
+
+   Default: ``rq``
+
+.. py:data:: ddtrace.config.rq_worker['service']
+
+   The service name reported by default for RQ spans from workers.
+
+   This option can also be set with the ``DD_SERVICE`` environment
+   variable.
+
+   Default: ``rq-worker``
+
+.. __: https://python-rq.org/
+
+"""
+import os
+
+from ddtrace import Pin
+from ddtrace import config
+
+from .. import trace_utils
+from ...ext import SpanTypes
+from ...propagation.http import HTTPPropagator
+from ...utils import get_argument_value
+from ...utils.formats import asbool
+
+
+__all__ = [
+    "patch",
+    "unpatch",
+]
+
+
+config._add(
+    "rq",
+    dict(
+        distributed_tracing_enabled=asbool(os.environ.get("DD_RQ_DISTRIBUTED_TRACING_ENABLED", True)),
+        _default_service="rq",
+    ),
+)
+
+config._add(
+    "rq_worker",
+    dict(
+        distributed_tracing_enabled=asbool(os.environ.get("DD_RQ_DISTRIBUTED_TRACING_ENABLED", True)),
+        _default_service="rq-worker",
+    ),
+)
+
+
+@trace_utils.with_traced_module
+def traced_queue_enqueue_job(rq, pin, func, instance, args, kwargs):
+    job = get_argument_value(args, kwargs, 0, "f")
+
+    func_name = job.func_name
+    job_inst = job.instance
+    job_inst_str = "%s.%s" % (job_inst.__module__, job_inst.__class__.__name__) if job_inst else ""
+
+    if job_inst_str:
+        resource = "%s.%s" % (job_inst_str, func_name)
+    else:
+        resource = func_name
+
+    with pin.tracer.trace(
+        "rq.queue.enqueue_job",
+        service=trace_utils.int_service(pin, config.rq),
+        resource=resource,
+        span_type=SpanTypes.WORKER,
+    ) as span:
+        span._set_str_tag("queue.name", instance.name)
+        span.set_tag("job.id", job.get_id())
+        span._set_str_tag("job.func_name", job.func_name)
+
+        # If the queue is_async then add distributed tracing headers to the job
+        if instance.is_async and config.rq.distributed_tracing_enabled:
+            HTTPPropagator.inject(span.context, job.meta)
+        return func(*args, **kwargs)
+
+
+@trace_utils.with_traced_module
+def traced_queue_fetch_job(rq, pin, func, instance, args, kwargs):
+    with pin.tracer.trace("rq.queue.fetch_job", service=trace_utils.int_service(pin, config.rq)) as span:
+        job_id = get_argument_value(args, kwargs, 0, "job_id")
+        span.set_tag("job.id", job_id)
+        return func(*args, **kwargs)
+
+
+@trace_utils.with_traced_module
+def traced_perform_job(rq, pin, func, instance, args, kwargs):
+    """Trace rq.Worker.perform_job"""
+    # `perform_job` is executed in a freshly forked, short-lived instance
+    job = get_argument_value(args, kwargs, 0, "job")
+
+    if config.rq_worker.distributed_tracing_enabled:
+        ctx = HTTPPropagator.extract(job.meta)
+        if ctx.trace_id:
+            pin.tracer.context_provider.activate(ctx)
+
+    try:
+        with pin.tracer.trace(
+            "rq.worker.perform_job",
+            service=trace_utils.int_service(pin, config.rq_worker),
+            span_type=SpanTypes.WORKER,
+            resource=job.func_name,
+        ) as span:
+            span.set_tag("job.id", job.get_id())
+            try:
+                return func(*args, **kwargs)
+            finally:
+                span.set_tag("job.status", job.get_status())
+                span.set_tag("job.origin", job.origin)
+                if job.is_failed:
+                    span.error = 1
+    finally:
+        # Force flush to agent since the process `os.exit()`s
+        # immediately after this method returns
+        pin.tracer.writer.flush_queue()
+
+
+@trace_utils.with_traced_module
+def traced_job_perform(rq, pin, func, instance, args, kwargs):
+    """Trace rq.Job.perform(...)"""
+    job = instance
+
+    # Inherit the service name from whatever parent exists.
+    # eg. in a worker, a perform_job parent span will exist with the worker
+    #     service.
+    with pin.tracer.trace("rq.job.perform", resource=job.func_name) as span:
+        span.set_tag("job.id", job.get_id())
+        return func(*args, **kwargs)
+
+
+@trace_utils.with_traced_module
+def traced_job_fetch_many(rq, pin, func, instance, args, kwargs):
+    """Trace rq.Job.fetch_many(...)"""
+    with pin.tracer.trace("rq.job.fetch_many", service=trace_utils.ext_service(pin, config.rq_worker)) as span:
+        job_ids = get_argument_value(args, kwargs, 0, "job_ids")
+        span.set_tag("job_ids", job_ids)
+        return func(*args, **kwargs)
+
+
+def patch():
+    # Avoid importing rq at the module level, eventually will be an import hook
+    import rq
+
+    if getattr(rq, "_datadog_patch", False):
+        return
+
+    Pin().onto(rq)
+
+    # Patch rq.job.Job
+    Pin().onto(rq.job.Job)
+    trace_utils.wrap(rq.job, "Job.perform", traced_job_perform(rq.job.Job))
+
+    # Patch rq.queue.Queue
+    Pin().onto(rq.queue.Queue)
+    trace_utils.wrap("rq.queue", "Queue.enqueue_job", traced_queue_enqueue_job(rq))
+    trace_utils.wrap("rq.queue", "Queue.fetch_job", traced_queue_fetch_job(rq))
+
+    # Patch rq.worker.Worker
+    Pin().onto(rq.worker.Worker)
+    trace_utils.wrap(rq.worker, "Worker.perform_job", traced_perform_job(rq))
+
+    setattr(rq, "_datadog_patch", True)
+
+
+def unpatch():
+    import rq
+
+    if not getattr(rq, "_datadog_patch", False):
+        return
+
+    Pin().remove_from(rq)
+
+    # Unpatch rq.job.Job
+    Pin().remove_from(rq.job.Job)
+    trace_utils.unwrap(rq.job.Job, "perform")
+
+    # Unpatch rq.queue.Queue
+    Pin().remove_from(rq.queue.Queue)
+    trace_utils.unwrap(rq.queue.Queue, "enqueue_job")
+    trace_utils.unwrap(rq.queue.Queue, "fetch_job")
+
+    # Unpatch rq.worker.Worker
+    Pin().remove_from(rq.worker.Worker)
+    trace_utils.unwrap(rq.worker.Worker, "perform_job")
+
+    setattr(rq, "_datadog_patch", False)
diff --git a/ddtrace/monkey.py b/ddtrace/monkey.py
--- a/ddtrace/monkey.py
+++ b/ddtrace/monkey.py
@@ -51,6 +51,7 @@
     "redis": True,
     "rediscluster": True,
     "requests": True,
+    "rq": True,
     "sanic": True,
     "sqlalchemy": False,  # Prefer DB client instrumentation
     "sqlite3": True,
diff --git a/riotfile.py b/riotfile.py
--- a/riotfile.py
+++ b/riotfile.py
@@ -1012,6 +1012,43 @@ def select_pys(min_version=MIN_PYTHON_VERSION, max_version=MAX_PYTHON_VERSION):
                 ),
             ],
         ),
+        Venv(
+            name="rq",
+            command="pytest tests/contrib/rq",
+            venvs=[
+                Venv(
+                    pys=select_pys(max_version="2.7"),
+                    pkgs={
+                        "rq": [
+                            "~=1.0.0",
+                            "~=1.1.0",
+                            "~=1.2.0",
+                            "~=1.3.0",
+                        ],
+                    },
+                ),
+                Venv(
+                    pys=select_pys(min_version="3.5"),
+                    pkgs={
+                        "rq": [
+                            "~=1.0.0",
+                            "~=1.1.0",
+                            "~=1.2.0",
+                            "~=1.3.0",
+                            "~=1.4.0",
+                            "~=1.5.0",
+                            "~=1.6.0",
+                            "~=1.7.0",
+                            "~=1.8.0",
+                            "~=1.9.0",
+                            latest,
+                        ],
+                        # https://github.com/rq/rq/issues/1469 rq [1.0,1.8] is incompatible with click 8.0+
+                        "click": "==7.1.2",
+                    },
+                ),
+            ],
+        ),
         Venv(
             name="httpx",
             pys=select_pys(min_version="3.6"),


Test patch:
diff --git a/tests/contrib/rq/__init__.py b/tests/contrib/rq/__init__.py
new file mode 100644
diff --git a/tests/contrib/rq/jobs.py b/tests/contrib/rq/jobs.py
new file mode 100644
--- /dev/null
+++ b/tests/contrib/rq/jobs.py
@@ -0,0 +1,18 @@
+class MyException(Exception):
+    pass
+
+
+def job_add1(x):
+    return x + 1
+
+
+def job_fail():
+    raise MyException("error")
+
+
+class JobClass(object):
+    def __call__(self, x):
+        return x * 2
+
+    def job_on_class(self, x):
+        return x / 2
diff --git a/tests/contrib/rq/test_rq.py b/tests/contrib/rq/test_rq.py
new file mode 100644
--- /dev/null
+++ b/tests/contrib/rq/test_rq.py
@@ -0,0 +1,149 @@
+import os
+import subprocess
+import time
+
+import pytest
+import redis
+import rq
+
+from ddtrace import Pin
+from ddtrace.contrib.rq import patch
+from ddtrace.contrib.rq import unpatch
+from tests.utils import override_config
+from tests.utils import snapshot
+from tests.utils import snapshot_context
+
+from ..config import REDIS_CONFIG
+from .jobs import JobClass
+from .jobs import job_add1
+from .jobs import job_fail
+
+
+# Span data which isn't static to ignore in the snapshots.
+snapshot_ignores = ["meta.job.id", "meta.error.stack"]
+
+
+@pytest.fixture()
+def connection():
+    yield redis.Redis(port=REDIS_CONFIG["port"])
+
+
+@pytest.fixture()
+def queue(connection):
+    patch()
+    try:
+        q = rq.Queue("q", connection=connection)
+        yield q
+    finally:
+        unpatch()
+
+
+@pytest.fixture()
+def sync_queue(connection):
+    patch()
+    try:
+        sync_q = rq.Queue("sync-q", is_async=False, connection=connection)
+        yield sync_q
+    finally:
+        unpatch()
+
+
+@snapshot(ignores=snapshot_ignores)
+def test_sync_queue_enqueue(sync_queue):
+    sync_queue.enqueue(job_add1, 1)
+
+
+@snapshot(ignores=snapshot_ignores)
+def test_queue_failing_job(sync_queue):
+    with pytest.raises(Exception):
+        sync_queue.enqueue(job_fail)
+
+
+@snapshot(ignores=snapshot_ignores)
+def test_sync_worker(queue):
+    job = queue.enqueue(job_add1, 1)
+    worker = rq.SimpleWorker([queue], connection=queue.connection)
+    worker.work(burst=True)
+    assert job.result == 2
+
+
+@snapshot(ignores=snapshot_ignores)
+def test_sync_worker_multiple_jobs(queue):
+    jobs = []
+    for i in range(3):
+        jobs.append(queue.enqueue(job_add1, i))
+    worker = rq.SimpleWorker([queue], connection=queue.connection)
+    worker.work(burst=True)
+    assert [job.result for job in jobs] == [1, 2, 3]
+
+
+@snapshot(ignores=snapshot_ignores)
+def test_sync_worker_config_service(queue):
+    job = queue.enqueue(job_add1, 10)
+    with override_config("rq_worker", dict(service="my-worker-svc")):
+        worker = rq.SimpleWorker([queue], connection=queue.connection)
+        worker.work(burst=True)
+    assert job.result == 11
+
+
+@snapshot(ignores=snapshot_ignores)
+def test_queue_pin_service(queue):
+    Pin.override(queue, service="my-pin-svc")
+    job = queue.enqueue(job_add1, 10)
+    worker = rq.SimpleWorker([queue], connection=queue.connection)
+    worker.work(burst=True)
+    assert job.result == 11
+
+
+@snapshot(ignores=snapshot_ignores)
+def test_sync_worker_pin_service(queue):
+    job = queue.enqueue(job_add1, 10)
+    worker = rq.SimpleWorker([queue], connection=queue.connection)
+    Pin.override(worker, service="my-pin-svc")
+    worker.work(burst=True)
+    assert job.result == 11
+
+
+@snapshot(ignores=snapshot_ignores)
+def test_worker_failing_job(queue):
+    queue.enqueue(job_fail)
+    worker = rq.SimpleWorker([queue], connection=queue.connection)
+    worker.work(burst=True)
+
+
+@snapshot(ignores=snapshot_ignores)
+def test_worker_class_job(queue):
+    queue.enqueue(JobClass().job_on_class, 2)
+    queue.enqueue(JobClass(), 4)
+    worker = rq.SimpleWorker([queue], connection=queue.connection)
+    worker.work(burst=True)
+
+
+@pytest.mark.parametrize(
+    "distributed_tracing_enabled,worker_service_name", [(None, "custom-worker-service"), (False, None)]
+)
+def test_enqueue(queue, distributed_tracing_enabled, worker_service_name):
+    token = "tests.contrib.rq.test_rq.test_enqueue_distributed_tracing_enabled_%s_worker_service_%s" % (
+        distributed_tracing_enabled,
+        worker_service_name,
+    )
+    with snapshot_context(token, ignores=snapshot_ignores):
+        env = os.environ.copy()
+        env["DD_TRACE_REDIS_ENABLED"] = "false"
+        if distributed_tracing_enabled is not None:
+            env["DD_RQ_DISTRIBUTED_TRACING_ENABLED"] = str(distributed_tracing_enabled)
+        if worker_service_name is not None:
+            env["DD_SERVICE"] = "custom-worker-service"
+        p = subprocess.Popen(["ddtrace-run", "rq", "worker", "q"], env=env)
+        try:
+            job = queue.enqueue(job_add1, 1)
+            # Wait for job to complete
+            for _ in range(100):
+                if job.result is not None:
+                    break
+                time.sleep(0.1)
+            assert job.result == 2
+        finally:
+            p.terminate()
+            # Wait for trace to be sent
+            time.sleep(0.5)
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_enqueue.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_enqueue.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_enqueue.snap
@@ -0,0 +1,45 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1631031023531799000
+   "duration" 7038000
+   "meta" {"runtime-id" "ef5a721a615c4641bd03dac6ee8e86d0"
+           "queue.name" "q"
+           "job.id" "0693f9d1-644e-49c2-bf17-7216bb73ea88"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 18236
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "custom-worker-service"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1631031025883804000
+       "duration" 20880000
+       "meta" {"runtime-id" "1cd5e25a1bd24b77bb978bc738242c34"
+               "job.id" "0693f9d1-644e-49c2-bf17-7216bb73ea88"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 18257
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "custom-worker-service"
+           "resource" "tests.contrib.rq.jobs.job_add1"
+           "error" 0
+           "span_id" 2
+           "trace_id" 0
+           "parent_id" 1
+           "start" 1631031025891219000
+           "duration" 4381000
+           "meta" {"job.id" "0693f9d1-644e-49c2-bf17-7216bb73ea88"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_enqueue_distributed_tracing_enabled_False_worker_service_None.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_enqueue_distributed_tracing_enabled_False_worker_service_None.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_enqueue_distributed_tracing_enabled_False_worker_service_None.snap
@@ -0,0 +1,46 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1631053637662518300
+   "duration" 2733000
+   "meta" {"runtime-id" "b1fd435dff564be79ebf4876f19e3401"
+           "queue.name" "q"
+           "job.id" "a15302ae-5332-4986-accf-4e5b8cb766fb"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 2666
+              "_dd.tracer_kr" 1.0}}]
+ [{"name" "rq.worker.perform_job"
+   "service" "rq-worker"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 1
+   "parent_id" nil
+   "start" 1631053640905464600
+   "duration" 21301700
+   "meta" {"runtime-id" "03a7167e3899435fa18ab9f018314093"
+           "job.id" "a15302ae-5332-4986-accf-4e5b8cb766fb"
+           "job.status" "finished"
+           "job.origin" "q"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 2679
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.job.perform"
+       "service" "rq-worker"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "error" 0
+       "span_id" 1
+       "trace_id" 1
+       "parent_id" 0
+       "start" 1631053640908800000
+       "duration" 13139700
+       "meta" {"job.id" "a15302ae-5332-4986-accf-4e5b8cb766fb"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_enqueue_distributed_tracing_enabled_None_worker_service_custom-worker-service.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_enqueue_distributed_tracing_enabled_None_worker_service_custom-worker-service.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_enqueue_distributed_tracing_enabled_None_worker_service_custom-worker-service.snap
@@ -0,0 +1,45 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1631053633351328100
+   "duration" 2363000
+   "meta" {"runtime-id" "b1fd435dff564be79ebf4876f19e3401"
+           "queue.name" "q"
+           "job.id" "24d5cedf-caa3-41a1-9763-32a79f1b2ca1"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 2666
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "custom-worker-service"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1631053636948434900
+       "duration" 63030800
+       "meta" {"runtime-id" "1a0fb63e68d94c12a364d691b87726c5"
+               "job.id" "24d5cedf-caa3-41a1-9763-32a79f1b2ca1"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 2674
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "custom-worker-service"
+           "resource" "tests.contrib.rq.jobs.job_add1"
+           "error" 0
+           "span_id" 2
+           "trace_id" 0
+           "parent_id" 1
+           "start" 1631053636953874500
+           "duration" 53669300
+           "meta" {"job.id" "24d5cedf-caa3-41a1-9763-32a79f1b2ca1"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_queue_failing_job.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_queue_failing_job.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_queue_failing_job.snap
@@ -0,0 +1,34 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_fail"
+   "type" "worker"
+   "error" 1
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1630333148384566000
+   "duration" 15223500
+   "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+           "queue.name" "sync-q"
+           "job.id" "86f976b4-38db-44da-9d72-6507557fff33"
+           "job.func_name" "tests.contrib.rq.jobs.job_fail"
+           "error.msg" "error"
+           "error.type" "tests.contrib.rq.jobs.MyException"
+           "error.stack" "Traceback (most recent call last):\n  File \"/root/project/ddtrace/contrib/rq/__init__.py\", line 122, in traced_queue_enqueue_job\n    return func(*args, **kwargs)\n  File \"/root/project/.riot/venv_py387_rq~100_redis30/lib/python3.8/site-packages/rq/queue.py\", line 356, in enqueue_job\n    job = self.run_job(job)\n  File \"/root/project/.riot/venv_py387_rq~100_redis30/lib/python3.8/site-packages/rq/queue.py\", line 282, in run_job\n    job.perform()\n  File \"/root/project/ddtrace/contrib/trace_utils.py\", line 161, in wrapper\n    return func(mod, pin, wrapped, instance, args, kwargs)\n  File \"/root/project/ddtrace/contrib/rq/__init__.py\", line 173, in traced_job_perform\n    return func(*args, **kwargs)\n  File \"/root/project/.riot/venv_py387_rq~100_redis30/lib/python3.8/site-packages/rq/job.py\", line 588, in perform\n    self._result = self._execute()\n  File \"/root/project/.riot/venv_py387_rq~100_redis30/lib/python3.8/site-packages/rq/job.py\", line 594, in _execute\n    return self.func(*self.args, **self.kwargs)\n  File \"/root/project/tests/contrib/rq/jobs.py\", line 10, in job_fail\n    raise MyException(\"error\")\ntests.contrib.rq.jobs.MyException: error\n"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 462
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.job.perform"
+       "service" "rq"
+       "resource" "tests.contrib.rq.jobs.job_fail"
+       "error" 1
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1630333148386640400
+       "duration" 8034000
+       "meta" {"job.id" "86f976b4-38db-44da-9d72-6507557fff33"
+               "error.msg" "error"
+               "error.type" "tests.contrib.rq.jobs.MyException"
+               "error.stack" "Traceback (most recent call last):\n  File \"/root/project/ddtrace/contrib/rq/__init__.py\", line 173, in traced_job_perform\n    return func(*args, **kwargs)\n  File \"/root/project/.riot/venv_py387_rq~100_redis30/lib/python3.8/site-packages/rq/job.py\", line 588, in perform\n    self._result = self._execute()\n  File \"/root/project/.riot/venv_py387_rq~100_redis30/lib/python3.8/site-packages/rq/job.py\", line 594, in _execute\n    return self.func(*self.args, **self.kwargs)\n  File \"/root/project/tests/contrib/rq/jobs.py\", line 10, in job_fail\n    raise MyException(\"error\")\ntests.contrib.rq.jobs.MyException: error\n"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_queue_pin_service.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_queue_pin_service.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_queue_pin_service.snap
@@ -0,0 +1,45 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "my-pin-svc"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1630333148767004500
+   "duration" 2762100
+   "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+           "queue.name" "q"
+           "job.id" "be8550fb-dec3-4121-87b1-c5340beb8853"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 462
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "rq-worker"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1630333148779048000
+       "duration" 7548800
+       "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+               "job.id" "be8550fb-dec3-4121-87b1-c5340beb8853"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 462
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "rq-worker"
+           "resource" "tests.contrib.rq.jobs.job_add1"
+           "error" 0
+           "span_id" 2
+           "trace_id" 0
+           "parent_id" 1
+           "start" 1630333148780572900
+           "duration" 878600
+           "meta" {"job.id" "be8550fb-dec3-4121-87b1-c5340beb8853"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_sync_queue_enqueue.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_sync_queue_enqueue.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_sync_queue_enqueue.snap
@@ -0,0 +1,28 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1631049654148153700
+   "duration" 23705500
+   "meta" {"runtime-id" "6e75d0807aa04f78aea66a630db7ac7f"
+           "queue.name" "sync-q"
+           "job.id" "5f0176f9-a10e-43a3-bc03-aced1bfae677"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 684
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.job.perform"
+       "service" "rq"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "error" 0
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1631049654165756500
+       "duration" 1037100
+       "meta" {"job.id" "5f0176f9-a10e-43a3-bc03-aced1bfae677"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker.snap
@@ -0,0 +1,45 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1630333148448191700
+   "duration" 4758100
+   "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+           "queue.name" "q"
+           "job.id" "8ecd683e-ad34-44f3-bb24-bb5b1cd60475"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 462
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "rq-worker"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1630333148477384300
+       "duration" 6117700
+       "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+               "job.id" "8ecd683e-ad34-44f3-bb24-bb5b1cd60475"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 462
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "rq-worker"
+           "resource" "tests.contrib.rq.jobs.job_add1"
+           "error" 0
+           "span_id" 2
+           "trace_id" 0
+           "parent_id" 1
+           "start" 1630333148478759900
+           "duration" 585100
+           "meta" {"job.id" "8ecd683e-ad34-44f3-bb24-bb5b1cd60475"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker_config_service.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker_config_service.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker_config_service.snap
@@ -0,0 +1,45 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1630333148705299400
+   "duration" 7544600
+   "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+           "queue.name" "q"
+           "job.id" "db432bb1-6acd-494d-a01f-b3ce14c2c2fb"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 462
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "my-worker-svc"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1630333148723234900
+       "duration" 7035700
+       "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+               "job.id" "db432bb1-6acd-494d-a01f-b3ce14c2c2fb"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 462
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "my-worker-svc"
+           "resource" "tests.contrib.rq.jobs.job_add1"
+           "error" 0
+           "span_id" 2
+           "trace_id" 0
+           "parent_id" 1
+           "start" 1630333148725016000
+           "duration" 1024100
+           "meta" {"job.id" "db432bb1-6acd-494d-a01f-b3ce14c2c2fb"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker_multiple_jobs.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker_multiple_jobs.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker_multiple_jobs.snap
@@ -0,0 +1,135 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1630333148531027200
+   "duration" 5560800
+   "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+           "queue.name" "q"
+           "job.id" "b10753b4-2d9d-4319-9edd-57dbf9157c0b"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 462
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "rq-worker"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1630333148552271200
+       "duration" 7096800
+       "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+               "job.id" "b10753b4-2d9d-4319-9edd-57dbf9157c0b"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 462
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "rq-worker"
+           "resource" "tests.contrib.rq.jobs.job_add1"
+           "error" 0
+           "span_id" 2
+           "trace_id" 0
+           "parent_id" 1
+           "start" 1630333148553750900
+           "duration" 625700
+           "meta" {"job.id" "b10753b4-2d9d-4319-9edd-57dbf9157c0b"}}]
+ [{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 1
+   "parent_id" nil
+   "start" 1630333148536887800
+   "duration" 4800100
+   "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+           "queue.name" "q"
+           "job.id" "c0feca4d-e80a-4d6a-a185-23c21c79bc38"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 462
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "rq-worker"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 1
+       "parent_id" 0
+       "start" 1630333148586676000
+       "duration" 12407800
+       "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+               "job.id" "c0feca4d-e80a-4d6a-a185-23c21c79bc38"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 462
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "rq-worker"
+           "resource" "tests.contrib.rq.jobs.job_add1"
+           "error" 0
+           "span_id" 2
+           "trace_id" 1
+           "parent_id" 1
+           "start" 1630333148589617700
+           "duration" 698400
+           "meta" {"job.id" "c0feca4d-e80a-4d6a-a185-23c21c79bc38"}}]
+ [{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 2
+   "parent_id" nil
+   "start" 1630333148541957500
+   "duration" 1202000
+   "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+           "queue.name" "q"
+           "job.id" "85b02c10-a395-42ea-9264-1b8b6c876321"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 462
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "rq-worker"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 2
+       "parent_id" 0
+       "start" 1630333148615392000
+       "duration" 11371900
+       "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+               "job.id" "85b02c10-a395-42ea-9264-1b8b6c876321"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 462
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "rq-worker"
+           "resource" "tests.contrib.rq.jobs.job_add1"
+           "error" 0
+           "span_id" 2
+           "trace_id" 2
+           "parent_id" 1
+           "start" 1630333148618904100
+           "duration" 662300
+           "meta" {"job.id" "85b02c10-a395-42ea-9264-1b8b6c876321"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker_pin_service.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker_pin_service.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_sync_worker_pin_service.snap
@@ -0,0 +1,45 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_add1"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1630333148845191200
+   "duration" 4423000
+   "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+           "queue.name" "q"
+           "job.id" "7f475cdd-3f19-4f23-86ad-c60a6d3dac0f"
+           "job.func_name" "tests.contrib.rq.jobs.job_add1"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 462
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "my-pin-svc"
+       "resource" "tests.contrib.rq.jobs.job_add1"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1630333148884054600
+       "duration" 38824600
+       "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+               "job.id" "7f475cdd-3f19-4f23-86ad-c60a6d3dac0f"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 462
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "my-pin-svc"
+           "resource" "tests.contrib.rq.jobs.job_add1"
+           "error" 0
+           "span_id" 2
+           "trace_id" 0
+           "parent_id" 1
+           "start" 1630333148888827500
+           "duration" 2674200
+           "meta" {"job.id" "7f475cdd-3f19-4f23-86ad-c60a6d3dac0f"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_worker_class_job.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_worker_class_job.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_worker_class_job.snap
@@ -0,0 +1,90 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.JobClass.job_on_class"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1630539839015211000
+   "duration" 11457000
+   "meta" {"runtime-id" "8140542a64974813b38ce3a795a83268"
+           "queue.name" "q"
+           "job.id" "3fed17e6-0034-416e-b7cc-ca41f69ad64e"
+           "job.func_name" "job_on_class"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 19332
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "rq-worker"
+       "resource" "job_on_class"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1630539839061506000
+       "duration" 10969000
+       "meta" {"runtime-id" "8140542a64974813b38ce3a795a83268"
+               "job.id" "3fed17e6-0034-416e-b7cc-ca41f69ad64e"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 19332
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "rq-worker"
+           "resource" "job_on_class"
+           "error" 0
+           "span_id" 2
+           "trace_id" 0
+           "parent_id" 1
+           "start" 1630539839063398000
+           "duration" 1287000
+           "meta" {"job.id" "3fed17e6-0034-416e-b7cc-ca41f69ad64e"}}]
+ [{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.JobClass.__call__"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 1
+   "parent_id" nil
+   "start" 1630539839028374000
+   "duration" 4931000
+   "meta" {"runtime-id" "8140542a64974813b38ce3a795a83268"
+           "queue.name" "q"
+           "job.id" "6ae53cab-d198-4e1a-851a-71b0f853bc63"
+           "job.func_name" "__call__"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 19332
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "rq-worker"
+       "resource" "__call__"
+       "type" "worker"
+       "error" 0
+       "span_id" 1
+       "trace_id" 1
+       "parent_id" 0
+       "start" 1630539839102215000
+       "duration" 9988000
+       "meta" {"runtime-id" "8140542a64974813b38ce3a795a83268"
+               "job.id" "6ae53cab-d198-4e1a-851a-71b0f853bc63"
+               "job.status" "finished"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 19332
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "rq-worker"
+           "resource" "__call__"
+           "error" 0
+           "span_id" 2
+           "trace_id" 1
+           "parent_id" 1
+           "start" 1630539839104129000
+           "duration" 1286000
+           "meta" {"job.id" "6ae53cab-d198-4e1a-851a-71b0f853bc63"}}]]
diff --git a/tests/snapshots/tests.contrib.rq.test_rq.test_worker_failing_job.snap b/tests/snapshots/tests.contrib.rq.test_rq.test_worker_failing_job.snap
new file mode 100644
--- /dev/null
+++ b/tests/snapshots/tests.contrib.rq.test_rq.test_worker_failing_job.snap
@@ -0,0 +1,48 @@
+[[{"name" "rq.queue.enqueue_job"
+   "service" "rq"
+   "resource" "tests.contrib.rq.jobs.job_fail"
+   "type" "worker"
+   "error" 0
+   "span_id" 0
+   "trace_id" 0
+   "parent_id" nil
+   "start" 1630333148970118000
+   "duration" 4247600
+   "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+           "queue.name" "q"
+           "job.id" "be34a76b-aa9e-455e-9b33-d98da5c66cd7"
+           "job.func_name" "tests.contrib.rq.jobs.job_fail"}
+   "metrics" {"_dd.agent_psr" 1.0
+              "_sampling_priority_v1" 1
+              "system.pid" 462
+              "_dd.tracer_kr" 1.0}}
+      {"name" "rq.worker.perform_job"
+       "service" "rq-worker"
+       "resource" "tests.contrib.rq.jobs.job_fail"
+       "type" "worker"
+       "error" 1
+       "span_id" 1
+       "trace_id" 0
+       "parent_id" 0
+       "start" 1630333148986879600
+       "duration" 18437200
+       "meta" {"runtime-id" "db58ccbfef044eeab2b2670b8b124068"
+               "job.id" "be34a76b-aa9e-455e-9b33-d98da5c66cd7"
+               "job.status" "failed"
+               "job.origin" "q"}
+       "metrics" {"_sampling_priority_v1" 1
+                  "system.pid" 462
+                  "_dd.tracer_kr" 1.0}}
+          {"name" "rq.job.perform"
+           "service" "rq-worker"
+           "resource" "tests.contrib.rq.jobs.job_fail"
+           "error" 1
+           "span_id" 2
+           "trace_id" 0
+           "parent_id" 1
+           "start" 1630333148988998200
+           "duration" 2697400
+           "meta" {"job.id" "be34a76b-aa9e-455e-9b33-d98da5c66cd7"
+                   "error.msg" "error"
+                   "error.type" "tests.contrib.rq.jobs.MyException"
+                   "error.stack" "Traceback (most recent call last):\n  File \"/root/project/ddtrace/contrib/rq/__init__.py\", line 173, in traced_job_perform\n    return func(*args, **kwargs)\n  File \"/root/project/.riot/venv_py387_rq~100_redis30/lib/python3.8/site-packages/rq/job.py\", line 588, in perform\n    self._result = self._execute()\n  File \"/root/project/.riot/venv_py387_rq~100_redis30/lib/python3.8/site-packages/rq/job.py\", line 594, in _execute\n    return self.func(*self.args, **self.kwargs)\n  File \"/root/project/tests/contrib/rq/jobs.py\", line 10, in job_fail\n    raise MyException(\"error\")\ntests.contrib.rq.jobs.MyException: error\n"}}]]