diff --git a/src/Http/TracedResponse.php b/src/Http/TracedResponse.php
index ea33d16..fb6650c 100644
--- a/src/Http/TracedResponse.php
+++ b/src/Http/TracedResponse.php
@@ -158,17 +158,14 @@ protected function endTracing(): void
         }
 
         try {
-            if (\in_array('response.headers', $info['user_data']['span_attributes'] ?? [])) {
-                $this->span->setAttribute('response.headers', HttpMessageHelper::formatHeadersForSpanAttribute($this->getHeaders(false)));
-            }
-
-            if (\in_array('response.body', $info['user_data']['span_attributes'] ?? [])) {
+            if (\is_callable($info['user_data']['on_response'] ?? null)) {
                 if (empty($this->content)) {
                     $stream = $this->toStream(false);
                     $this->content = stream_get_contents($stream) ?: null;
                     rewind($stream);
                 }
-                $this->span->setAttribute('response.body', $this->content);
+
+                \call_user_func($info['user_data']['on_response'], $this->getHeaders(false), $this->content, $this->span);
             }
         } catch (\Throwable) {
         }
diff --git a/src/Http/TracingHttpClient.php b/src/Http/TracingHttpClient.php
index bd0fb20..c358772 100644
--- a/src/Http/TracingHttpClient.php
+++ b/src/Http/TracingHttpClient.php
@@ -14,6 +14,7 @@
 use Instrumentation\Semantics\OperationName\ClientRequestOperationNameResolver;
 use Instrumentation\Semantics\OperationName\ClientRequestOperationNameResolverInterface;
 use Instrumentation\Tracing\Tracing;
+use OpenTelemetry\API\Trace\SpanInterface;
 use OpenTelemetry\API\Trace\SpanKind;
 use OpenTelemetry\API\Trace\StatusCode;
 use OpenTelemetry\SDK\Common\Time\ClockInterface;
@@ -56,34 +57,15 @@ public function __construct(
         $this->attributeProvider = $attributeProvider ?: new ClientRequestAttributeProvider();
     }
 
-    /**
-     * @param array<string> $attributes
-     *
-     * @return array<string>
-     */
-    protected function getExtraSpanAttributes(array|null $attributes): array
-    {
-        $attributes = $attributes ?: $_SERVER['OTEL_PHP_HTTP_SPAN_ATTRIBUTES'] ?? [];
-
-        if (\is_string($attributes)) {
-            $attributes = explode(',', $attributes);
-        }
-
-        if (!\is_array($attributes)) {
-            throw new \RuntimeException(\sprintf('Extra span attributes must be a comma separated list of attributes or an array. %s given.', get_debug_type($attributes)));
-        }
-
-        return $attributes;
-    }
-
     /**
      * @param array{
      *     on_progress?: ?callable,
      *     headers?: array<string,array<string>>,
      *     extra?: array{
      *         operation_name: non-empty-string,
-     *         span_attributes: array<non-empty-string>,
-     *         extra_attributes: array<non-empty-string, string>
+     *         extra_attributes: array<non-empty-string, string>,
+     *         on_request: callable(array<string,array<string>>, string|null, SpanInterface): void,
+     *         on_response: callable(array<string,array<string>>, string|null, SpanInterface): void,
      *     }
      * } $options
      */
@@ -106,22 +88,17 @@ public function request(string $method, string $url, array $options = []): Respo
 
         $attributes = $this->attributeProvider->getAttributes($method, $url, $headers);
         $attributes += $options['extra']['extra_attributes'] ?? [];
+        $span->setAttributes($attributes);
 
         $scope->detach();
 
-        $options['user_data']['span_attributes'] = $this->getExtraSpanAttributes($options['extra']['span_attributes'] ?? null);
-
-        try {
-            if (\in_array('request.body', $options['user_data']['span_attributes']) && $body = self::getRequestBody($options)) {
-                $attributes['request.body'] = $body;
-            }
-            if (\in_array('request.headers', $options['user_data']['span_attributes'])) {
-                $attributes['request.headers'] = HttpMessageHelper::formatHeadersForSpanAttribute($headers);
-            }
-        } catch (\Throwable) {
+        if (\is_callable($options['extra']['on_request'] ?? null)) {
+            \call_user_func($options['extra']['on_request'], $headers, self::getRequestBody($options), $span);
         }
 
-        $span->setAttributes($attributes);
+        if (isset($options['extra']['on_response'])) {
+            $options['user_data']['on_response'] = $options['extra']['on_response'];
+        }
 
         $options = array_merge($options, [
             'on_progress' => function ($dlNow, $dlSize, $info) use ($onProgress, $span, $options) {
diff --git a/src/Metrics/HistogramAdapter.php b/src/Metrics/HistogramAdapter.php
index 7e33b09..1e1ad77 100644
--- a/src/Metrics/HistogramAdapter.php
+++ b/src/Metrics/HistogramAdapter.php
@@ -8,6 +8,7 @@
 namespace Instrumentation\Metrics;
 
 use OpenTelemetry\API\Metrics\HistogramInterface;
+use OpenTelemetry\Context\ContextInterface;
 use Prometheus\CollectorRegistry;
 use Prometheus\Histogram;
 
@@ -23,7 +24,7 @@ public function __construct(
     /**
      * @param array{labels: array<string,mixed>, buckets?: array<int>} $attributes
      */
-    public function record($amount, iterable $attributes = [], $context = null): void
+    public function record($amount, iterable $attributes = [], ContextInterface|false|null $context = null): void
     {
         if (!\is_array($attributes)) {
             return;