From a991351710c5257c920357604e083805a30b93ad Mon Sep 17 00:00:00 2001
From: Anthonin Bonnefoy <anthonin.bonnefoy@datadoghq.com>
Date: Mon, 15 May 2023 10:00:08 +0200
Subject: Add pg_tracing extension to contrib

This extension adds the possibility to create server-side spans to be
used for distributed tracing.

For this first version, we rely on SQLCommenter to propagate the trace
context. Other methods of trace propagation (ex: through a GUC variable)
can be added as a follow-up.

The sampling decision to trace a query or not relies on either a global
sampling rate or a sampling rate based on the sampled flag from
SQLCommenter's tracecontext. Additional methods of filtering and tuning
the sampling decision (ex: filtering on queryId) could be added as a
afterward.

When a query is sampled, a top span will be generated using the query
string and used as a parent for the other query's spans. Spans will be
generated for other query functions like Planner, ExecutorRun and
ExecutorFinish.

The spans are exposed through the pg_tracing_peek_spans and
pg_tracing_consume_spans views. Sending spans to a trace collector is
is outside this extension's scope.
---
 contrib/Makefile                              |    1 +
 contrib/meson.build                           |    1 +
 contrib/pg_tracing/Makefile                   |   26 +
 contrib/pg_tracing/expected/cleanup.out       |    8 +
 contrib/pg_tracing/expected/extended.out      |  133 ++
 contrib/pg_tracing/expected/full_buffer.out   |   55 +
 contrib/pg_tracing/expected/insert.out        |   49 +
 contrib/pg_tracing/expected/nested.out        |  183 ++
 contrib/pg_tracing/expected/reset.out         |   13 +
 contrib/pg_tracing/expected/sample.out        |  154 ++
 contrib/pg_tracing/expected/select.out        |  267 +++
 contrib/pg_tracing/expected/subxact.out       |   43 +
 contrib/pg_tracing/expected/trigger.out       |  246 ++
 contrib/pg_tracing/expected/utility.out       |  270 +++
 contrib/pg_tracing/expected/wal.out           |   25 +
 contrib/pg_tracing/meson.build                |   53 +
 contrib/pg_tracing/pg_tracing--1.0.sql        |  101 +
 contrib/pg_tracing/pg_tracing.c               | 2047 +++++++++++++++++
 contrib/pg_tracing/pg_tracing.conf            |    2 +
 contrib/pg_tracing/pg_tracing.control         |    5 +
 contrib/pg_tracing/pg_tracing.h               |  196 ++
 contrib/pg_tracing/pg_tracing_query_process.c |  534 +++++
 contrib/pg_tracing/pg_tracing_span.c          |  261 +++
 contrib/pg_tracing/sql/cleanup.sql            |    5 +
 contrib/pg_tracing/sql/extended.sql           |   41 +
 contrib/pg_tracing/sql/full_buffer.sql        |   30 +
 contrib/pg_tracing/sql/insert.sql             |   22 +
 contrib/pg_tracing/sql/nested.sql             |  129 ++
 contrib/pg_tracing/sql/reset.sql              |    3 +
 contrib/pg_tracing/sql/sample.sql             |   49 +
 contrib/pg_tracing/sql/select.sql             |  103 +
 contrib/pg_tracing/sql/subxact.sql            |   18 +
 contrib/pg_tracing/sql/trigger.sql            |  171 ++
 contrib/pg_tracing/sql/utility.sql            |  189 ++
 contrib/pg_tracing/sql/wal.sql                |   14 +
 doc/src/sgml/contrib.sgml                     |    1 +
 doc/src/sgml/filelist.sgml                    |    1 +
 doc/src/sgml/pgtracing.sgml                   |  872 +++++++
 38 files changed, 6321 insertions(+)
 create mode 100644 contrib/pg_tracing/Makefile
 create mode 100644 contrib/pg_tracing/expected/cleanup.out
 create mode 100644 contrib/pg_tracing/expected/extended.out
 create mode 100644 contrib/pg_tracing/expected/full_buffer.out
 create mode 100644 contrib/pg_tracing/expected/insert.out
 create mode 100644 contrib/pg_tracing/expected/nested.out
 create mode 100644 contrib/pg_tracing/expected/reset.out
 create mode 100644 contrib/pg_tracing/expected/sample.out
 create mode 100644 contrib/pg_tracing/expected/select.out
 create mode 100644 contrib/pg_tracing/expected/subxact.out
 create mode 100644 contrib/pg_tracing/expected/trigger.out
 create mode 100644 contrib/pg_tracing/expected/utility.out
 create mode 100644 contrib/pg_tracing/expected/wal.out
 create mode 100644 contrib/pg_tracing/meson.build
 create mode 100644 contrib/pg_tracing/pg_tracing--1.0.sql
 create mode 100644 contrib/pg_tracing/pg_tracing.c
 create mode 100644 contrib/pg_tracing/pg_tracing.conf
 create mode 100644 contrib/pg_tracing/pg_tracing.control
 create mode 100644 contrib/pg_tracing/pg_tracing.h
 create mode 100644 contrib/pg_tracing/pg_tracing_query_process.c
 create mode 100644 contrib/pg_tracing/pg_tracing_span.c
 create mode 100644 contrib/pg_tracing/sql/cleanup.sql
 create mode 100644 contrib/pg_tracing/sql/extended.sql
 create mode 100644 contrib/pg_tracing/sql/full_buffer.sql
 create mode 100644 contrib/pg_tracing/sql/insert.sql
 create mode 100644 contrib/pg_tracing/sql/nested.sql
 create mode 100644 contrib/pg_tracing/sql/reset.sql
 create mode 100644 contrib/pg_tracing/sql/sample.sql
 create mode 100644 contrib/pg_tracing/sql/select.sql
 create mode 100644 contrib/pg_tracing/sql/subxact.sql
 create mode 100644 contrib/pg_tracing/sql/trigger.sql
 create mode 100644 contrib/pg_tracing/sql/utility.sql
 create mode 100644 contrib/pg_tracing/sql/wal.sql
 create mode 100644 doc/src/sgml/pgtracing.sgml

diff --git a/contrib/Makefile b/contrib/Makefile
index abd780f277..1cd56c310e 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -35,6 +35,7 @@ SUBDIRS = \
 		pg_prewarm	\
 		pg_stat_statements \
 		pg_surgery	\
+		pg_tracing		\
 		pg_trgm		\
 		pgrowlocks	\
 		pgstattuple	\
diff --git a/contrib/meson.build b/contrib/meson.build
index 14a8906865..92e2e46a0f 100644
--- a/contrib/meson.build
+++ b/contrib/meson.build
@@ -51,6 +51,7 @@ subdir('pgrowlocks')
 subdir('pg_stat_statements')
 subdir('pgstattuple')
 subdir('pg_surgery')
+subdir('pg_tracing')
 subdir('pg_trgm')
 subdir('pg_visibility')
 subdir('pg_walinspect')
diff --git a/contrib/pg_tracing/Makefile b/contrib/pg_tracing/Makefile
new file mode 100644
index 0000000000..11d3927007
--- /dev/null
+++ b/contrib/pg_tracing/Makefile
@@ -0,0 +1,26 @@
+# contrib/pg_tracing/Makefile
+
+MODULE_big = pg_tracing
+OBJS = \
+	$(WIN32RES) \
+	pg_tracing.o \
+	pg_tracing_query_process.o \
+	pg_tracing_span.o
+
+EXTENSION = pg_tracing
+DATA = pg_tracing--1.0.sql
+PGFILEDESC = "pg_tracing - Distributed tracing for postgres"
+
+REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_tracing/pg_tracing.conf
+REGRESS = utility select extended insert trigger sample subxact full_buffer nested wal cleanup
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_tracing
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_tracing/expected/cleanup.out b/contrib/pg_tracing/expected/cleanup.out
new file mode 100644
index 0000000000..7ab66964ef
--- /dev/null
+++ b/contrib/pg_tracing/expected/cleanup.out
@@ -0,0 +1,8 @@
+-- Drop test table and test functions
+DROP TABLE pg_tracing_test;
+DROP function test_function_project_set;
+DROP function test_function_result;
+DROP EXTENSION pg_tracing CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to view peek_spans_with_level
+drop cascades to view peek_ordered_spans
diff --git a/contrib/pg_tracing/expected/extended.out b/contrib/pg_tracing/expected/extended.out
new file mode 100644
index 0000000000..90400d76e2
--- /dev/null
+++ b/contrib/pg_tracing/expected/extended.out
@@ -0,0 +1,133 @@
+-- Trace everything
+SET pg_tracing.sample_rate = 1.0;
+-- Simple query with extended protocol
+SELECT $1, $2 \bind 1 2 \g
+ ?column? | ?column? 
+----------+----------
+ 1        | 2
+(1 row)
+
+SELECT span_type, span_operation, parameters, lvl FROM peek_ordered_spans;
+  span_type   | span_operation |     parameters     | lvl 
+--------------+----------------+--------------------+-----
+ Select query | SELECT $1, $2  | $1 = '1', $2 = '2' |   1
+ Planner      | Planner        |                    |   2
+ Executor     | ExecutorRun    |                    |   2
+(3 rows)
+
+CALL clean_spans();
+-- Trigger an error due to mismatching number of parameters
+BEGIN; select $1 \bind \g
+ERROR:  bind message supplies 0 parameters, but prepared statement "" requires 1
+ROLLBACK;
+SELECT span_type, span_operation, parameters, lvl FROM peek_ordered_spans;
+   span_type    | span_operation | parameters | lvl 
+----------------+----------------+------------+-----
+ Utility query  | BEGIN;         |            |   1
+ ProcessUtility | ProcessUtility |            |   2
+ Utility query  | ROLLBACK;      |            |   1
+ ProcessUtility | ProcessUtility |            |   2
+(4 rows)
+
+CALL clean_spans();
+-- Execute queries with extended protocol within an explicit transaction
+BEGIN;
+SELECT $1 \bind 1 \g
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT $1, $2 \bind 2 3 \g
+ ?column? | ?column? 
+----------+----------
+ 2        | 3
+(1 row)
+
+COMMIT;
+-- Spans within the same transaction should have been generated with the same trace_id
+SELECT count(distinct(trace_id)) = 1 FROM pg_tracing_peek_spans;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT span_type, span_operation, parameters, lvl FROM peek_ordered_spans;
+   span_type    | span_operation |     parameters     | lvl 
+----------------+----------------+--------------------+-----
+ Utility query  | BEGIN;         |                    |   1
+ ProcessUtility | ProcessUtility |                    |   2
+ Select query   | SELECT $1      | $1 = '1'           |   1
+ Planner        | Planner        |                    |   2
+ Executor       | ExecutorRun    |                    |   2
+ Select query   | SELECT $1, $2  | $1 = '2', $2 = '3' |   1
+ Planner        | Planner        |                    |   2
+ Executor       | ExecutorRun    |                    |   2
+ Utility query  | COMMIT;        |                    |   1
+ ProcessUtility | ProcessUtility |                    |   2
+(10 rows)
+
+CALL clean_spans();
+-- Mix extended protocol and simple protocol
+BEGIN;
+SELECT $1 \bind 1 \g
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT 5, 6, 7;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+        5 |        6 |        7
+(1 row)
+
+SELECT $1, $2 \bind 2 3 \g
+ ?column? | ?column? 
+----------+----------
+ 2        | 3
+(1 row)
+
+COMMIT;
+-- Spans within the same transaction should have been generated with the same trace_id
+SELECT count(distinct(trace_id)) = 1 FROM pg_tracing_peek_spans;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT span_type, span_operation, parameters, lvl FROM peek_ordered_spans;
+   span_type    |   span_operation   |       parameters       | lvl 
+----------------+--------------------+------------------------+-----
+ Utility query  | BEGIN;             |                        |   1
+ ProcessUtility | ProcessUtility     |                        |   2
+ Select query   | SELECT $1          | $1 = '1'               |   1
+ Planner        | Planner            |                        |   2
+ Executor       | ExecutorRun        |                        |   2
+ Select query   | SELECT $1, $2, $3; | $1 = 5, $2 = 6, $3 = 7 |   1
+ Planner        | Planner            |                        |   2
+ Executor       | ExecutorRun        |                        |   2
+ Select query   | SELECT $1, $2      | $1 = '2', $2 = '3'     |   1
+ Planner        | Planner            |                        |   2
+ Executor       | ExecutorRun        |                        |   2
+ Utility query  | COMMIT;            |                        |   1
+ ProcessUtility | ProcessUtility     |                        |   2
+(13 rows)
+
+CALL clean_spans();
+-- gdesc calls a single parse command then execute a query. Make sure we handle this case
+SELECT 1 \gdesc
+  Column  |  Type   
+----------+---------
+ ?column? | integer
+(1 row)
+
+SELECT span_type, span_operation, parameters, lvl FROM peek_ordered_spans;
+  span_type   |                                                        span_operation                                                        |             parameters              | lvl 
+--------------+------------------------------------------------------------------------------------------------------------------------------+-------------------------------------+-----
+ Select query | SELECT name AS "Column", pg_catalog.format_type(tp, tpm) AS "Type"FROM (VALUES ($1, $2::pg_catalog.oid,$3)) s(name, tp, tpm) | $1 = '?column?', $2 = '23', $3 = -1 |   1
+ Planner      | Planner                                                                                                                      |                                     |   2
+ Executor     | ExecutorRun                                                                                                                  |                                     |   2
+(3 rows)
+
+CALL clean_spans();
diff --git a/contrib/pg_tracing/expected/full_buffer.out b/contrib/pg_tracing/expected/full_buffer.out
new file mode 100644
index 0000000000..6079a275b8
--- /dev/null
+++ b/contrib/pg_tracing/expected/full_buffer.out
@@ -0,0 +1,55 @@
+-- A simple procedure creating nested calls
+CREATE OR REPLACE PROCEDURE loop_select(iterations int) AS
+$BODY$
+BEGIN
+    FOR i IN 1..iterations LOOP
+        PERFORM 'SELECT 1;';
+    END LOOP;
+END;
+$BODY$
+LANGUAGE plpgsql;
+-- Clear stats
+select * from pg_tracing_reset();
+ pg_tracing_reset 
+------------------
+ 
+(1 row)
+
+-- Check initial stats after reset
+select processed_traces, processed_spans, dropped_traces, dropped_spans from pg_tracing_info();
+ processed_traces | processed_spans | dropped_traces | dropped_spans 
+------------------+-----------------+----------------+---------------
+                0 |               0 |              0 |             0
+(1 row)
+
+-- Saturate the span buffer. Each call should create at least 2 spans
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ CALL loop_select(20);
+-- Check that we have dropped spans. The trace was still partially processed
+select processed_traces = 1, processed_spans = 40, dropped_traces = 0, dropped_spans > 0 from pg_tracing_info();
+ ?column? | ?column? | ?column? | ?column? 
+----------+----------+----------+----------
+ t        | t        | t        | t
+(1 row)
+
+-- Try to create new traces while buffer is full
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ SELECT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-00'*/ SELECT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+-- We should have only one additional dropped trace
+select processed_traces = 1, processed_spans = 40, dropped_traces = 1 from pg_tracing_info();
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+-- Clean current spans
+CALL clean_spans();
diff --git a/contrib/pg_tracing/expected/insert.out b/contrib/pg_tracing/expected/insert.out
new file mode 100644
index 0000000000..bcffbc3a7e
--- /dev/null
+++ b/contrib/pg_tracing/expected/insert.out
@@ -0,0 +1,49 @@
+-- Only trace queries with sample flag
+SET pg_tracing.sample_rate = 0.0;
+SET pg_tracing.caller_sample_rate = 1.0;
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ CREATE TABLE IF NOT EXISTS pg_tracing_test_table_with_constraint (a int, b char(20), CONSTRAINT PK_tracing_test PRIMARY KEY (a));
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000001';
+   span_type    |                                                          span_operation                                                           | lvl 
+----------------+-----------------------------------------------------------------------------------------------------------------------------------+-----
+ Utility query  | CREATE TABLE IF NOT EXISTS pg_tracing_test_table_with_constraint (a int, b char(20), CONSTRAINT PK_tracing_test PRIMARY KEY (a)); |   1
+ ProcessUtility | ProcessUtility                                                                                                                    |   2
+ Utility query  | CREATE TABLE IF NOT EXISTS pg_tracing_test_table_with_constraint (a int, b char(20), CONSTRAINT PK_tracing_test PRIMARY KEY (a)); |   3
+ ProcessUtility | ProcessUtility                                                                                                                    |   4
+ Utility query  | CREATE TABLE IF NOT EXISTS pg_tracing_test_table_with_constraint (a int, b char(20), CONSTRAINT PK_tracing_test PRIMARY KEY (a)); |   3
+ ProcessUtility | ProcessUtility                                                                                                                    |   4
+(6 rows)
+
+-- Simple insertion
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ INSERT INTO pg_tracing_test_table_with_constraint VALUES(1, 'aaa');
+SELECT span_type, span_operation from peek_ordered_spans where trace_id='00000000000000000000000000000002';
+  span_type   |                          span_operation                           
+--------------+-------------------------------------------------------------------
+ Insert query | INSERT INTO pg_tracing_test_table_with_constraint VALUES($1, $2);
+ Planner      | Planner
+ Executor     | ExecutorRun
+(3 rows)
+
+-- Trigger constraint violation
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-01'*/ INSERT INTO pg_tracing_test_table_with_constraint VALUES(1, 'aaa');
+ERROR:  duplicate key value violates unique constraint "pk_tracing_test"
+DETAIL:  Key (a)=(1) already exists.
+SELECT span_type, span_operation, sql_error_code, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000003';
+  span_type   |                          span_operation                           | sql_error_code | lvl 
+--------------+-------------------------------------------------------------------+----------------+-----
+ Insert query | INSERT INTO pg_tracing_test_table_with_constraint VALUES($1, $2); | 23505          |   1
+ Planner      | Planner                                                           | 00000          |   2
+ Executor     | ExecutorRun                                                       | 23505          |   2
+(3 rows)
+
+-- Trigger an error while calling pg_tracing_peek_spans which resets tracing, nothing should be generated
+CALL clean_spans();
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000004-0000000000000004-01'*/ INSERT INTO pg_tracing_test_table_with_constraint VALUES(length((select sql_error_code from public.pg_tracing_peek_spans)), 'aaa');
+ERROR:  null value in column "a" of relation "pg_tracing_test_table_with_constraint" violates not-null constraint
+DETAIL:  Failing row contains (null, aaa                 ).
+SELECT span_type, span_operation, sql_error_code from peek_ordered_spans where trace_id='00000000000000000000000000000004';
+ span_type | span_operation | sql_error_code 
+-----------+----------------+----------------
+(0 rows)
+
+-- Cleanup
+CALL clean_spans();
diff --git a/contrib/pg_tracing/expected/nested.out b/contrib/pg_tracing/expected/nested.out
new file mode 100644
index 0000000000..3ae7f627c3
--- /dev/null
+++ b/contrib/pg_tracing/expected/nested.out
@@ -0,0 +1,183 @@
+-- Create test function to sample
+CREATE OR REPLACE FUNCTION test_function_project_set(a int) RETURNS SETOF oid AS
+$BODY$
+BEGIN
+	RETURN QUERY SELECT oid from pg_class where oid = a;
+END;
+$BODY$
+LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION test_function_result(a int, b text) RETURNS void AS
+$BODY$
+BEGIN
+    INSERT INTO pg_tracing_test(a, b) VALUES (a, b);
+END;
+$BODY$
+LANGUAGE plpgsql;
+-- Trace a statement with a function call
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000051-0000000000000051-01'*/ select test_function_project_set(1);
+ test_function_project_set 
+---------------------------
+(0 rows)
+
+-- The test function call will generate the following spans (non exhaustive list):
+-- +------------------------------------------------------------+
+-- | A: Select test_function_project_set(1);                    |
+-- +-+----------+-+------------------------------------------+--+
+--   |C: Planner| |E: ExecutorRun                            |
+--   +----------+ +-----------+------------------------------+
+--                            | I: Select a from b where...  |
+--                            +---+--------------+-----------+
+--                                |J: ExecutorRun|
+--                                +--------------+
+-- Gather span_id, span start and span end of function call statement
+SELECT span_id AS span_a_id,
+        get_span_start(span_start) as span_a_start,
+        get_span_end(span_start) as span_a_end
+		from pg_tracing_peek_spans where parent_id='0000000000000051' \gset
+SELECT span_id AS span_e_id,
+        get_span_start(span_start) as span_e_start,
+        get_span_end(span_start) as span_e_end
+		from pg_tracing_peek_spans where parent_id=:'span_a_id' and span_type='Executor' and span_operation='ExecutorRun' \gset
+SELECT span_id AS span_i_id,
+        get_span_start(span_start) as span_i_start,
+        get_span_end(span_start) as span_i_end
+		from pg_tracing_peek_spans where parent_id=:'span_e_id' and span_type='Select query' \gset
+SELECT span_id AS span_j_id,
+        get_span_start(span_start) as span_j_start,
+        get_span_end(span_start) as span_j_end
+		from pg_tracing_peek_spans where parent_id=:'span_i_id' and span_operation='ExecutorRun' \gset
+-- Check that spans' start and end are within expection
+SELECT :span_a_start <= :span_e_start AS top_query_before_run,
+		:span_a_end >= :span_e_end AS top_ends_after_run_end;
+ top_query_before_run | top_ends_after_run_end 
+----------------------+------------------------
+ t                    | t
+(1 row)
+
+-- Check that the root span is the longest one
+WITH max_end AS (select max(span_end) from pg_tracing_peek_spans)
+SELECT span_end = max_end.max from pg_tracing_peek_spans, max_end
+    where span_id = :'span_a_id';
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Check tracking with top tracking
+SET pg_tracing.track = 'top';
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000052-0000000000000052-01'*/ select test_function_project_set(1);
+ test_function_project_set 
+---------------------------
+(0 rows)
+
+SELECT count(*) from pg_tracing_consume_spans where trace_id='00000000000000000000000000000052';
+ count 
+-------
+     3
+(1 row)
+
+-- Check tracking with no tracking
+SET pg_tracing.track = 'none';
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000053-0000000000000053-01'*/ select test_function_project_set(1);
+ test_function_project_set 
+---------------------------
+(0 rows)
+
+SELECT count(*) from pg_tracing_consume_spans where trace_id='00000000000000000000000000000053';
+ count 
+-------
+     0
+(1 row)
+
+-- Reset tracking setting
+SET pg_tracing.track TO DEFAULT;
+-- Create test procedure
+CREATE OR REPLACE PROCEDURE sum_one(i int) AS $$
+DECLARE
+  r int;
+BEGIN
+  SELECT (i + i)::int INTO r;
+END; $$ LANGUAGE plpgsql;
+-- Test tracking of procedure with utility tracking enabled
+SET pg_tracing.track_utility=on;
+/*traceparent='00-00000000000000000000000000000054-0000000000000054-01'*/ CALL sum_one(3);
+select span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000054';
+   span_operation    | lvl 
+---------------------+-----
+ CALL sum_one($1);   |   1
+ ProcessUtility      |   2
+ SELECT (i + i)::int |   3
+ Planner             |   4
+ Planner             |   4
+ ExecutorRun         |   4
+(6 rows)
+
+-- Test again with utility tracking disabled
+SET pg_tracing.track_utility=off;
+/*traceparent='00-00000000000000000000000000000055-0000000000000055-01'*/ CALL sum_one(10);
+select span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000055';
+ span_operation | lvl 
+----------------+-----
+(0 rows)
+
+-- Create immutable function
+CREATE OR REPLACE FUNCTION test_immutable_function(a int) RETURNS oid
+AS 'SELECT oid from pg_class where oid = a;'
+LANGUAGE sql IMMUTABLE;
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000056-0000000000000056-01'*/ select test_immutable_function(1);
+ test_immutable_function 
+-------------------------
+                        
+(1 row)
+
+select span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000056';
+           span_operation            | lvl 
+-------------------------------------+-----
+ select test_immutable_function($1); |   1
+ Planner                             |   2
+ Planner                             |   3
+ ExecutorRun                         |   2
+(4 rows)
+
+-- +-------------------------------------------------------------------+
+-- | A: Select test_function(1);                                       |
+-- +-+----------++----------------++-------------------------------+---+
+--   |C: Planner||D: ExecutorStart||E: ExecutorRun                 |
+--   +----------++----------------+++------------------------------+
+--                                   |H: Insert INTO...    |
+--                                   +--+--------------+---+
+--                                      |I: ExecutorRun|
+--                                      +--------------+
+-- Check function with result node
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000058-0000000000000058-01'*/ select test_function_result(1, 'test');
+ test_function_result 
+----------------------
+ 
+(1 row)
+
+-- Gather span_id, span start and span end of function call statement
+SELECT span_id AS span_a_id,
+        get_span_start(span_start) as span_a_start,
+        get_span_end(span_start) as span_a_end
+		from pg_tracing_peek_spans where parent_id='0000000000000058' \gset
+SELECT span_id AS span_e_id,
+        get_span_start(span_start) as span_e_start,
+        get_span_end(span_start) as span_e_end
+		from pg_tracing_peek_spans where parent_id=:'span_a_id' and span_type='Executor' and span_operation='ExecutorRun' \gset
+SELECT span_id AS span_h_id,
+        get_span_start(span_start) as span_h_start,
+        get_span_end(span_start) as span_h_end
+		from pg_tracing_peek_spans where parent_id=:'span_e_id' and span_type='Insert query' \gset
+select span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000058';
+                 span_operation                  | lvl 
+-------------------------------------------------+-----
+ select test_function_result($1, $2);            |   1
+ Planner                                         |   2
+ ExecutorRun                                     |   2
+ INSERT INTO pg_tracing_test(a, b) VALUES (a, b) |   3
+ Planner                                         |   4
+ ExecutorRun                                     |   4
+(6 rows)
+
+-- Cleanup
+CALL clean_spans();
diff --git a/contrib/pg_tracing/expected/reset.out b/contrib/pg_tracing/expected/reset.out
new file mode 100644
index 0000000000..2e0c903121
--- /dev/null
+++ b/contrib/pg_tracing/expected/reset.out
@@ -0,0 +1,13 @@
+-- Check reset is working
+SELECT pg_tracing_reset();
+ pg_tracing_reset 
+------------------
+ 
+(1 row)
+
+SELECT traces from pg_tracing_info;
+ traces 
+--------
+      0
+(1 row)
+
diff --git a/contrib/pg_tracing/expected/sample.out b/contrib/pg_tracing/expected/sample.out
new file mode 100644
index 0000000000..3648340498
--- /dev/null
+++ b/contrib/pg_tracing/expected/sample.out
@@ -0,0 +1,154 @@
+-- Trace nothing
+SET pg_tracing.sample_rate = 0.0;
+SET pg_tracing.caller_sample_rate = 0.0;
+-- Query with sampling flag
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ SELECT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+select count(distinct(trace_id))=0 from pg_tracing_consume_spans;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Query without trace context
+SELECT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+select count(distinct(trace_id))=0 from pg_tracing_consume_spans;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Enable full sampling
+SET pg_tracing.sample_rate = 1.0;
+-- Generate queries with sampling flag on, off and no trace context
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ SELECT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-00'*/ SELECT 2;
+ ?column? 
+----------
+        2
+(1 row)
+
+SELECT 3;
+ ?column? 
+----------
+        3
+(1 row)
+
+SELECT 4;
+ ?column? 
+----------
+        4
+(1 row)
+
+-- Check number of generated spans
+select count(distinct(trace_id)) from pg_tracing_peek_spans;
+ count 
+-------
+     4
+(1 row)
+
+-- Check span order for full sampling
+select span_operation, parameters, lvl from peek_ordered_spans;
+ span_operation | parameters | lvl 
+----------------+------------+-----
+ SELECT $1;     | $1 = 1     |   1
+ Planner        |            |   2
+ ExecutorRun    |            |   2
+ SELECT $1;     | $1 = 2     |   1
+ Planner        |            |   2
+ ExecutorRun    |            |   2
+ SELECT $1;     | $1 = 3     |   1
+ Planner        |            |   2
+ ExecutorRun    |            |   2
+ SELECT $1;     | $1 = 4     |   1
+ Planner        |            |   2
+ ExecutorRun    |            |   2
+(12 rows)
+
+-- Top spans should reuse generated ids and have trace_id = parent_id
+select span_operation, parameters from peek_ordered_spans where right(trace_id, 16) = parent_id;
+ span_operation | parameters 
+----------------+------------
+ SELECT $1;     | $1 = 1
+ SELECT $1;     | $1 = 2
+ SELECT $1;     | $1 = 3
+ SELECT $1;     | $1 = 4
+(4 rows)
+
+CALL clean_spans();
+-- Only trace query with sampled flag
+SET pg_tracing.sample_rate = 0.0;
+SET pg_tracing.caller_sample_rate = 1.0;
+-- Clean set call spans
+CALL clean_spans();
+-- Generate queries with sampling flag on, off, no trace context and SQLComment at the end
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000004-0000000000000004-01'*/ SELECT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000005-0000000000000005-00'*/ SELECT 2;
+ ?column? 
+----------
+        2
+(1 row)
+
+SELECT 3 /*dddbs='postgres.db',traceparent='00-00000000000000000000000000000006-0000000000000006-01'*/;
+ ?column? 
+----------
+        3
+(1 row)
+
+/*dddbs='postgres.db',traceparent='00-fffffffffffffffff000000000000007-0000000000000007-01'*/ SELECT 4;
+ ?column? 
+----------
+        4
+(1 row)
+
+SELECT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+-- Check number of generated spans
+select distinct(trace_id) from pg_tracing_peek_spans order by trace_id;
+             trace_id             
+----------------------------------
+ 00000000000000000000000000000004
+ 00000000000000000000000000000006
+ fffffffffffffffff000000000000007
+(3 rows)
+
+-- Check span order for sampled flag only
+select span_operation, parameters, lvl from peek_ordered_spans;
+ span_operation | parameters | lvl 
+----------------+------------+-----
+ SELECT $1;     | $1 = 1     |   1
+ Planner        |            |   2
+ ExecutorRun    |            |   2
+ SELECT $1;     | $1 = 3     |   1
+ Planner        |            |   2
+ ExecutorRun    |            |   2
+ SELECT $1;     | $1 = 4     |   1
+ Planner        |            |   2
+ ExecutorRun    |            |   2
+(9 rows)
+
+-- Cleaning
+CALL clean_spans();
diff --git a/contrib/pg_tracing/expected/select.out b/contrib/pg_tracing/expected/select.out
new file mode 100644
index 0000000000..ab87f9265b
--- /dev/null
+++ b/contrib/pg_tracing/expected/select.out
@@ -0,0 +1,267 @@
+-- Only trace queries with sample flag
+SET pg_tracing.sample_rate = 0.0;
+SET pg_tracing.caller_sample_rate = 1.0;
+-- Run a simple query
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ SELECT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+-- Get top span id
+SELECT span_id AS top_span_id from pg_tracing_peek_spans where parent_id='0000000000000001' and span_type!='Parse' \gset
+-- Check parameters
+SELECT parameters from pg_tracing_peek_spans where span_id=:'top_span_id';
+ parameters 
+------------
+ $1 = 1
+(1 row)
+
+-- Check the number of children
+SELECT count(*) from pg_tracing_peek_spans where parent_id=:'top_span_id';
+ count 
+-------
+     2
+(1 row)
+
+-- Check span_operation
+SELECT span_type, span_operation from pg_tracing_peek_spans where trace_id='00000000000000000000000000000001' order by span_start, span_end desc;
+  span_type   | span_operation 
+--------------+----------------
+ Select query | SELECT $1;
+ Planner      | Planner
+ Executor     | ExecutorRun
+(3 rows)
+
+-- Check userid
+SELECT userid = (SELECT usesysid FROM pg_user WHERE usename = current_user) FROM pg_tracing_peek_spans GROUP BY userid;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Check dbid
+SELECT dbid = (SELECT oid FROM pg_database WHERE datname = (SELECT current_database())) FROM pg_tracing_peek_spans GROUP BY dbid;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Check count of query_id
+SELECT count(distinct query_id) from pg_tracing_consume_spans where trace_id='00000000000000000000000000000001';
+ count 
+-------
+     1
+(1 row)
+
+-- Get initial number of traces reported
+SELECT processed_traces from pg_tracing_info \gset
+-- Trace a statement with function call
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-01'*/ SELECT count(*) from current_database();
+ count 
+-------
+     1
+(1 row)
+
+-- Check the generated span span_type, span_operation and order of function call
+SELECT span_type, span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000003';
+  span_type   |              span_operation              | lvl 
+--------------+------------------------------------------+-----
+ Select query | SELECT count(*) from current_database(); |   1
+ Planner      | Planner                                  |   2
+ Executor     | ExecutorRun                              |   2
+(3 rows)
+
+-- Check expected reported number of trace
+SELECT processed_traces = :processed_traces + 1 from pg_tracing_info;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Trace a more complex query with multiple function calls
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000004-0000000000000004-01'*/ SELECT s.relation_size + s.index_size
+FROM (SELECT
+      pg_relation_size(C.oid) as relation_size,
+      pg_indexes_size(C.oid) as index_size
+    FROM pg_class C) as s limit 1;
+ ?column? 
+----------
+        0
+(1 row)
+
+-- Check the nested level of spans for a query with multiple function calls
+SELECT span_type, span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000004';
+  span_type   |                                                                          span_operation                                                                          | lvl 
+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----
+ Select query | SELECT s.relation_size + s.index_sizeFROM (SELECT pg_relation_size(C.oid) as relation_size, pg_indexes_size(C.oid) as index_size FROM pg_class C) as s limit $1; |   1
+ Planner      | Planner                                                                                                                                                          |   2
+ Executor     | ExecutorRun                                                                                                                                                      |   2
+(3 rows)
+
+-- Check that we're in a correct state after a timeout
+set statement_timeout=200;
+-- Trace query triggering a statement timeout
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000007-0000000000000007-01'*/ select * from pg_sleep(10);
+ERROR:  canceling statement due to statement timeout
+SELECT span_type, span_operation, sql_error_code, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000007';
+  span_type   |       span_operation        | sql_error_code | lvl 
+--------------+-----------------------------+----------------+-----
+ Select query | select * from pg_sleep($1); | 57014          |   1
+ Planner      | Planner                     | 00000          |   2
+ Executor     | ExecutorRun                 | 57014          |   2
+(3 rows)
+
+-- Cleanup statement setting
+set statement_timeout=0;
+-- Trace a working query after the timeout to check we're in a consistent state
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000008-0000000000000008-01'*/ select 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+-- Check the spans order and error code
+SELECT span_type, span_operation, sql_error_code, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000008';
+  span_type   | span_operation | sql_error_code | lvl 
+--------------+----------------+----------------+-----
+ Select query | select $1;     | 00000          |   1
+ Planner      | Planner        | 00000          |   2
+ Executor     | ExecutorRun    | 00000          |   2
+(3 rows)
+
+-- Cleanup
+SET plan_cache_mode='auto';
+-- Run a statement with node not executed
+/*dddbs='postgres.db',traceparent='00-0000000000000000000000000000000b-000000000000000b-01'*/ select 1 limit 0;
+ ?column? 
+----------
+(0 rows)
+
+SELECT span_operation, parameters, lvl from peek_ordered_spans where trace_id='0000000000000000000000000000000b';
+   span_operation    |   parameters   | lvl 
+---------------------+----------------+-----
+ select $1 limit $2; | $1 = 1, $2 = 0 |   1
+ Planner             |                |   2
+ ExecutorRun         |                |   2
+(3 rows)
+
+-- Test multiple statements in a single query
+/*dddbs='postgres.db',traceparent='00-0000000000000000000000000000000c-000000000000000c-01'*/ select 1; select 2;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? 
+----------
+        2
+(1 row)
+
+SELECT span_operation, parameters, lvl from peek_ordered_spans where trace_id='0000000000000000000000000000000c';
+ span_operation | parameters | lvl 
+----------------+------------+-----
+ select $1;     | $1 = 1     |   1
+ Planner        |            |   2
+ ExecutorRun    |            |   2
+(3 rows)
+
+-- Check that parameters are not exported when disabled
+SET pg_tracing.export_parameters=false;
+/*dddbs='postgres.db',traceparent='00-0000000000000000000000000000000d-000000000000000d-01'*/ select 1, 2, 3;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+        1 |        2 |        3
+(1 row)
+
+SELECT span_operation, parameters, lvl from peek_ordered_spans where trace_id='0000000000000000000000000000000d';
+   span_operation   | parameters | lvl 
+--------------------+------------+-----
+ select $1, $2, $3; |            |   1
+ Planner            |            |   2
+ ExecutorRun        |            |   2
+(3 rows)
+
+SET pg_tracing.export_parameters=true;
+-- Check multi statement query
+CALL clean_spans();
+SET pg_tracing.sample_rate = 1.0;
+-- Force a multi-query statement with \;
+SELECT 1\; SELECT 1, 2;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        2
+(1 row)
+
+SELECT span_type, span_operation, parameters, lvl from peek_ordered_spans;
+  span_type   | span_operation | parameters | lvl 
+--------------+----------------+------------+-----
+ Select query | SELECT $1;     | $1 = 1     |   1
+ Planner      | Planner        |            |   2
+ Executor     | ExecutorRun    |            |   2
+(3 rows)
+
+CALL clean_spans();
+-- Check standalone trace
+SELECT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+-- Make sure we have unique span ids
+SELECT count(span_id) from pg_tracing_consume_spans group by span_id;
+ count 
+-------
+     1
+     1
+     1
+(3 rows)
+
+-- Trigger a planner error
+SELECT '\xDEADBEEF'::bytea::text::int;
+ERROR:  invalid input syntax for type integer: "\xdeadbeef"
+-- Check planner error
+SELECT span_type, span_operation, parameters, sql_error_code, lvl from peek_ordered_spans;
+  span_type   |        span_operation        |    parameters     | sql_error_code | lvl 
+--------------+------------------------------+-------------------+----------------+-----
+ Select query | SELECT $1::bytea::text::int; | $1 = '\xDEADBEEF' | 22P02          |   1
+ Planner      | Planner                      |                   | 22P02          |   2
+(2 rows)
+
+CALL clean_spans();
+-- Check spans generated by lazy functions
+CREATE OR REPLACE FUNCTION lazy_function(IN anyarray, OUT x anyelement)
+    RETURNS SETOF anyelement
+    LANGUAGE sql
+    AS 'select * from pg_catalog.generate_series(array_lower($1, 1), array_upper($1, 1), 1)';
+SELECT lazy_function('{1,2,3,4}'::int[]) FROM (VALUES (1,2));
+ lazy_function 
+---------------
+             1
+             2
+             3
+             4
+(4 rows)
+
+-- Check lazy function spans
+SELECT span_type, span_operation, parameters, lvl from peek_ordered_spans;
+   span_type    |                                                                                             span_operation                                                                                              |            parameters            | lvl 
+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------+-----
+ Utility query  | CREATE OR REPLACE FUNCTION lazy_function(IN anyarray, OUT x anyelement) RETURNS SETOF anyelement LANGUAGE sql AS 'select * from pg_catalog.generate_series(array_lower($1, 1), array_upper($1, 1), 1)'; |                                  |   1
+ ProcessUtility | ProcessUtility                                                                                                                                                                                          |                                  |   2
+ Select query   | SELECT lazy_function($1::int[]) FROM (VALUES ($2,$3));                                                                                                                                                  | $1 = '{1,2,3,4}', $2 = 1, $3 = 2 |   1
+ Planner        | Planner                                                                                                                                                                                                 |                                  |   2
+ Executor       | ExecutorRun                                                                                                                                                                                             |                                  |   2
+ Select query   | select * from pg_catalog.generate_series(array_lower($1, $2), array_upper($1, $3), $4)                                                                                                                  | $1 = 1, $2 = 1, $3 = 1           |   3
+ Planner        | Planner                                                                                                                                                                                                 |                                  |   4
+(7 rows)
+
+-- Cleanup
+SET pg_tracing.sample_rate = 0.0;
+CALL clean_spans();
diff --git a/contrib/pg_tracing/expected/subxact.out b/contrib/pg_tracing/expected/subxact.out
new file mode 100644
index 0000000000..a434a97cad
--- /dev/null
+++ b/contrib/pg_tracing/expected/subxact.out
@@ -0,0 +1,43 @@
+-- Enable full sampling
+SET pg_tracing.sample_rate = 1.0;
+-- Start a transaction with subxaction
+BEGIN;
+SAVEPOINT s1;
+INSERT INTO pg_tracing_test VALUES(generate_series(1, 2), 'aaa');
+SAVEPOINT s2;
+INSERT INTO pg_tracing_test VALUES(generate_series(1, 2), 'aaa');
+SAVEPOINT s3;
+SELECT 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+COMMIT;
+-- Check that subxact_count is correctly reported
+select span_operation, parameters, subxact_count, lvl FROM peek_ordered_spans;
+                          span_operation                          |         parameters         | subxact_count | lvl 
+------------------------------------------------------------------+----------------------------+---------------+-----
+ BEGIN;                                                           |                            |             0 |   1
+ ProcessUtility                                                   |                            |             0 |   2
+ SAVEPOINT $1;                                                    | $1 = s1                    |             0 |   1
+ ProcessUtility                                                   |                            |             0 |   2
+ INSERT INTO pg_tracing_test VALUES(generate_series($1, $2), $3); | $1 = 1, $2 = 2, $3 = 'aaa' |             0 |   1
+ Planner                                                          |                            |             0 |   2
+ ExecutorRun                                                      |                            |             0 |   2
+ SAVEPOINT $1;                                                    | $1 = s2                    |             1 |   1
+ ProcessUtility                                                   |                            |             1 |   2
+ INSERT INTO pg_tracing_test VALUES(generate_series($1, $2), $3); | $1 = 1, $2 = 2, $3 = 'aaa' |             1 |   1
+ Planner                                                          |                            |             1 |   2
+ ExecutorRun                                                      |                            |             1 |   2
+ SAVEPOINT $1;                                                    | $1 = s3                    |             2 |   1
+ ProcessUtility                                                   |                            |             2 |   2
+ SELECT $1;                                                       | $1 = 1                     |             2 |   1
+ Planner                                                          |                            |             2 |   2
+ ExecutorRun                                                      |                            |             2 |   2
+ COMMIT;                                                          |                            |             2 |   1
+ ProcessUtility                                                   |                            |             2 |   2
+(19 rows)
+
+-- Cleaning
+CALL clean_spans();
diff --git a/contrib/pg_tracing/expected/trigger.out b/contrib/pg_tracing/expected/trigger.out
new file mode 100644
index 0000000000..9c0b11ae6b
--- /dev/null
+++ b/contrib/pg_tracing/expected/trigger.out
@@ -0,0 +1,246 @@
+-- Only trace queries with sample flag
+SET pg_tracing.sample_rate = 0.0;
+SET pg_tracing.caller_sample_rate = 1.0;
+-- Create test tables
+CREATE TABLE Employee (
+    EmployeeId INT NOT NULL,
+    LastName VARCHAR(20) NOT NULL,
+    CONSTRAINT PK_Employee PRIMARY KEY (EmployeeId));
+CREATE TABLE Employee_Audit (
+    EmployeeId INT NOT NULL,
+    LastName VARCHAR(20) NOT NULL,
+    EmpAdditionTime VARCHAR(20) NOT NULL);
+-- Create trigger functions
+CREATE OR REPLACE FUNCTION employee_insert_trigger_fnc()
+  RETURNS trigger AS
+$$
+BEGIN
+    INSERT INTO Employee_Audit (EmployeeId, LastName, EmpAdditionTime)
+         VALUES(NEW.EmployeeId,NEW.LastName,current_date);
+RETURN NEW;
+END;
+$$
+LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION before_trigger_fnc()
+  RETURNS trigger AS
+$$
+BEGIN
+    PERFORM 'SELECT 1';
+RETURN NEW;
+END;
+$$
+LANGUAGE plpgsql;
+-- Hook the insert trigger twice after each row
+CREATE TRIGGER employee_insert_trigger
+  AFTER INSERT
+  ON Employee
+  FOR EACH ROW
+  EXECUTE PROCEDURE employee_insert_trigger_fnc();
+CREATE TRIGGER employee_insert_trigger_2
+  AFTER INSERT
+  ON Employee
+  FOR EACH ROW
+  EXECUTE PROCEDURE employee_insert_trigger_fnc();
+-- Call insert
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ INSERT INTO Employee VALUES(10,'Adams');
+--
+-- +--------------------------------------------------------------------------------------------------------+
+-- | B: INSERT INTO Employee                                                                                |
+-- ++----------+-+--------------+---------------------------------------------------------------------------+
+--  |C: Planner| |D: ExecutorRun| G: ExecutorFinish                                                       |
+--  +----------+ +--------------+--+-----------------------------------+-+--------------------------------+
+--                                 |I: INSERT INTO Employee_Audit...   | |N: INSERT INTO Employee_Audit...|
+--                                 +-+----------++--------------------++ +--------------------------------+
+--                                   |J: Planner||K: ExecutorRun      |  |O: ExecutorRun |
+--                                   +----------++--------------------+  +---------------+
+-- Gather span_id, span start and span end of call with triggers
+SELECT span_id AS span_b_id,
+        get_span_start(span_start) as span_b_start,
+        get_span_end(span_start) as span_b_end
+		from pg_tracing_peek_spans where parent_id='0000000000000001' \gset
+SELECT span_id AS span_c_id,
+        get_span_start(span_start) as span_c_start,
+        get_span_end(span_start) as span_c_end
+		from pg_tracing_peek_spans where parent_id=:'span_b_id' and span_type='Planner' \gset
+SELECT span_id AS span_d_id,
+        get_span_start(span_start) as span_d_start,
+        get_span_end(span_start) as span_d_end
+		from pg_tracing_peek_spans where parent_id=:'span_b_id' and span_type='Executor' and span_operation='ExecutorRun' \gset
+-- Executor Finish, first trigger
+SELECT span_id AS span_g_id,
+        get_span_start(span_start) as span_g_start,
+        get_span_end(span_start) as span_g_end
+		from pg_tracing_peek_spans where parent_id=:'span_b_id' and span_type='Executor' and span_operation='ExecutorFinish' \gset
+SELECT span_id AS span_i_id,
+        get_span_start(span_start) as span_i_start,
+        get_span_end(span_start) as span_i_end
+		from pg_tracing_peek_spans where parent_id=:'span_g_id' and span_type='Insert query' limit 1 \gset
+SELECT span_id AS span_j_id,
+        get_span_start(span_start) as span_j_start,
+        get_span_end(span_start) as span_j_end
+		from pg_tracing_peek_spans where parent_id=:'span_i_id' and span_type='Planner' \gset
+SELECT span_id AS span_k_id,
+        get_span_start(span_start) as span_k_start,
+        get_span_end(span_start) as span_k_end
+		from pg_tracing_peek_spans where parent_id=:'span_i_id' and span_type='Executor' and span_operation='ExecutorRun' \gset
+-- Executor Finish, second trigger
+SELECT span_id AS span_n_id,
+        get_span_start(span_start) as span_n_start,
+        get_span_end(span_start) as span_n_end
+        from pg_tracing_peek_spans where parent_id=:'span_g_id' and span_type='Insert query' limit 1 offset 1 \gset
+SELECT span_id AS span_o_id,
+        get_span_start(span_start) as span_o_start,
+        get_span_end(span_start) as span_o_end
+        from pg_tracing_peek_spans where parent_id=:'span_n_id' and span_type='Executor' and span_operation='ExecutorRun' \gset
+-- Check that spans' start and end are within expection
+-- Planner
+SELECT :span_c_start >= :span_b_start as planner_starts_after_root_span;
+ planner_starts_after_root_span 
+--------------------------------
+ t
+(1 row)
+
+-- ExecutorRun
+SELECT :span_d_start >= :span_c_end as executor_run_starts_after_planner,
+       :span_d_end <= :span_g_start as executor_run_ends_before_finish;
+ executor_run_starts_after_planner | executor_run_ends_before_finish 
+-----------------------------------+---------------------------------
+ t                                 | t
+(1 row)
+
+-- ExecutorFinish
+SELECT :span_g_start <= :span_i_start as executor_finish_starts_before_child,
+       :span_g_end >= :span_o_end as executor_finish_ends_after_child;
+ executor_finish_starts_before_child | executor_finish_ends_after_child 
+-------------------------------------+----------------------------------
+ t                                   | t
+(1 row)
+
+-- First trigger
+SELECT :span_i_end >= :span_n_end as first_trigger_ends_after_child;
+ first_trigger_ends_after_child 
+--------------------------------
+ t
+(1 row)
+
+-- Second trigger
+SELECT :span_o_start >= :span_i_end  as second_trigger_starts_after_first,
+       :span_o_end >= :span_o_end as second_trigger_ends_after_child,
+       :span_o_end <= :span_g_end as second_trigger_ends_before_executorfinish_end,
+       :span_n_end <= :span_g_end as second_trigger_ends_before_root_executorfinish_end;
+ second_trigger_starts_after_first | second_trigger_ends_after_child | second_trigger_ends_before_executorfinish_end | second_trigger_ends_before_root_executorfinish_end 
+-----------------------------------+---------------------------------+-----------------------------------------------+----------------------------------------------------
+ t                                 | t                               | t                                             | t
+(1 row)
+
+-- Check span_operation
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000001';
+  span_type   |                                                   span_operation                                                    | lvl 
+--------------+---------------------------------------------------------------------------------------------------------------------+-----
+ Insert query | INSERT INTO Employee VALUES($1,$2);                                                                                 |   1
+ Planner      | Planner                                                                                                             |   2
+ Executor     | ExecutorRun                                                                                                         |   2
+ Executor     | ExecutorFinish                                                                                                      |   2
+ Insert query | INSERT INTO Employee_Audit (EmployeeId, LastName, EmpAdditionTime) VALUES(NEW.EmployeeId,NEW.LastName,current_date) |   3
+ Planner      | Planner                                                                                                             |   4
+ Executor     | ExecutorRun                                                                                                         |   4
+ Insert query | INSERT INTO Employee_Audit (EmployeeId, LastName, EmpAdditionTime) VALUES(NEW.EmployeeId,NEW.LastName,current_date) |   3
+ Planner      | Planner                                                                                                             |   4
+ Executor     | ExecutorRun                                                                                                         |   4
+(10 rows)
+
+-- Check count of query_id
+SELECT count(distinct query_id) from pg_tracing_consume_spans where trace_id='00000000000000000000000000000001';
+ count 
+-------
+     2
+(1 row)
+
+-- Test table to test before trigger
+CREATE TABLE before_trigger_table (a int);
+-- Hook the before trigger
+CREATE TRIGGER employee_insert_trigger
+  BEFORE INSERT
+  ON before_trigger_table
+  FOR EACH ROW
+  EXECUTE PROCEDURE before_trigger_fnc();
+CREATE TRIGGER employee_update_trigger
+  BEFORE UPDATE
+  ON before_trigger_table
+  FOR EACH ROW
+  EXECUTE PROCEDURE before_trigger_fnc();
+CREATE TRIGGER employee_delete_trigger
+  BEFORE DELETE
+  ON before_trigger_table
+  FOR EACH ROW
+  EXECUTE PROCEDURE before_trigger_fnc();
+-- Test explain on a before trigger
+-- This will go through ExecutorEnd without any parent executor run
+/*dddbs='postgres.db',traceparent='00-fed00000000000000000000000000001-0000000000000003-01'*/ explain INSERT INTO before_trigger_table VALUES(10);
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Insert on before_trigger_table  (cost=0.00..0.01 rows=0 width=0)
+   ->  Result  (cost=0.00..0.01 rows=1 width=4)
+(2 rows)
+
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='fed00000000000000000000000000001';
+   span_type    |                    span_operation                    | lvl 
+----------------+------------------------------------------------------+-----
+ Utility query  | explain INSERT INTO before_trigger_table VALUES($1); |   1
+ ProcessUtility | ProcessUtility                                       |   2
+ Insert query   | explain INSERT INTO before_trigger_table VALUES($1); |   3
+ Planner        | Planner                                              |   4
+(4 rows)
+
+-- Call before trigger insert
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ INSERT INTO before_trigger_table SELECT generate_series(1,2);
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000002';
+  span_type   |                         span_operation                          | lvl 
+--------------+-----------------------------------------------------------------+-----
+ Insert query | INSERT INTO before_trigger_table SELECT generate_series($1,$2); |   1
+ Planner      | Planner                                                         |   2
+ Executor     | ExecutorRun                                                     |   2
+ Select query | SELECT $1                                                       |   3
+ Planner      | Planner                                                         |   4
+ Executor     | ExecutorRun                                                     |   4
+ Select query | SELECT 'SELECT 1'                                               |   3
+ Executor     | ExecutorRun                                                     |   4
+(8 rows)
+
+-- Call before trigger update
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-01'*/ UPDATE before_trigger_table set a=1;
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000003';
+  span_type   |            span_operation             | lvl 
+--------------+---------------------------------------+-----
+ Update query | UPDATE before_trigger_table set a=$1; |   1
+ Planner      | Planner                               |   2
+ Executor     | ExecutorRun                           |   2
+ Select query | SELECT $1                             |   3
+ Planner      | Planner                               |   4
+ Executor     | ExecutorRun                           |   4
+ Select query | SELECT 'SELECT 1'                     |   3
+ Executor     | ExecutorRun                           |   4
+(8 rows)
+
+-- Call before trigger delete
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000004-0000000000000004-01'*/ DELETE FROM before_trigger_table where a=1;
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000004';
+  span_type   |                span_operation                | lvl 
+--------------+----------------------------------------------+-----
+ Delete query | DELETE FROM before_trigger_table where a=$1; |   1
+ Planner      | Planner                                      |   2
+ Executor     | ExecutorRun                                  |   2
+ Select query | SELECT $1                                    |   3
+ Planner      | Planner                                      |   4
+ Executor     | ExecutorRun                                  |   4
+ Select query | SELECT 'SELECT 1'                            |   3
+ Executor     | ExecutorRun                                  |   4
+(8 rows)
+
+-- Check count of query_id
+SELECT count(distinct query_id) from pg_tracing_consume_spans;
+ count 
+-------
+     6
+(1 row)
+
diff --git a/contrib/pg_tracing/expected/utility.out b/contrib/pg_tracing/expected/utility.out
new file mode 100644
index 0000000000..bf4d787ad6
--- /dev/null
+++ b/contrib/pg_tracing/expected/utility.out
@@ -0,0 +1,270 @@
+-- Some helper functions
+CREATE OR REPLACE FUNCTION get_span_start(time_start timestamptz) RETURNS bigint AS
+$BODY$
+    SELECT extract(epoch from time_start);
+$BODY$
+LANGUAGE sql;
+CREATE OR REPLACE FUNCTION get_span_end(time_end timestamptz) RETURNS bigint AS
+$BODY$
+BEGIN
+    RETURN extract(epoch from time_end);
+END;
+$BODY$
+LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION get_span_end(time_end timestamptz) RETURNS bigint AS
+$BODY$
+BEGIN
+    RETURN extract(epoch from time_end);
+END;
+$BODY$
+LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE clean_spans() AS $$
+BEGIN
+    PERFORM count(*) from pg_tracing_consume_spans;
+END;
+$$ LANGUAGE plpgsql;
+-- Create pg_tracing extension with sampling on
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ CREATE EXTENSION pg_tracing;
+-- This will create the following spans (non exhaustive list):
+--
+-- +-------------------------------------------------------------------------------------+
+-- | A: Utility: Create Extension                                                        |
+-- +-+-----------------------------------------------------------------------------------+
+--   +----------------------------------------------------------------------------------+
+--   |B: ProcessUtility: Create Extension                                               |
+--   +---+-----------------------------------+---+--------------------------------------+
+--       +-----------------------------------+   +-------------------------------------+
+--       |C: Utility: Create Function1       |   |E: Utility: Create Function2         |
+--       ++----------------------------------+   ++-----------------------------------++
+--        +----------------------------------+    +-----------------------------------+
+--        |D: ProcessUtility: Create Function|    |F: ProcessUtility: Create Function2|
+--        +----------------------------------+    +-----------------------------------+
+-- Extract span_ids, start and end of those spans
+SELECT span_id AS span_a_id,
+        get_span_start(span_start) as span_a_start,
+        get_span_end(span_start) as span_a_end
+		from pg_tracing_peek_spans where parent_id='0000000000000001' and span_type='Utility query' \gset
+SELECT span_id AS span_b_id,
+        get_span_start(span_start) as span_b_start,
+        get_span_end(span_start) as span_b_end
+		from pg_tracing_peek_spans where parent_id=:'span_a_id' and span_type='ProcessUtility' \gset
+SELECT span_id AS span_c_id,
+        get_span_start(span_start) as span_c_start,
+        get_span_end(span_start) as span_c_end
+		from pg_tracing_peek_spans where parent_id=:'span_b_id' and span_type='Utility query' limit 1 \gset
+SELECT span_id AS span_d_id,
+        get_span_start(span_start) as span_d_start,
+        get_span_end(span_start) as span_d_end
+		from pg_tracing_peek_spans where parent_id=:'span_c_id' and span_type='ProcessUtility' \gset
+SELECT span_id AS span_e_id,
+        get_span_start(span_start) as span_e_start,
+        get_span_end(span_start) as span_e_end
+		from pg_tracing_peek_spans where parent_id=:'span_b_id' and span_type='Utility query' limit 1 offset 1 \gset
+-- Check that the start and end of those spans are within expectation
+SELECT :span_a_start <= :span_b_start AS span_a_starts_first,
+		:span_a_end >= :span_b_end AS span_a_ends_last,
+		:span_d_end <= :span_c_end AS nested_span_ends_before_parent,
+		:span_c_end <= :span_e_start AS next_utility_starts_after;
+ span_a_starts_first | span_a_ends_last | nested_span_ends_before_parent | next_utility_starts_after 
+---------------------+------------------+--------------------------------+---------------------------
+ t                   | t                | t                              | t
+(1 row)
+
+-- Clean current spans
+CALL clean_spans();
+--
+-- Test that no utility is captured with track_utility off
+--
+-- Set utility off
+SET pg_tracing.track_utility = off;
+-- Test utility tracking disabled + full sampling
+SET pg_tracing.sample_rate = 1.0;
+DROP EXTENSION pg_tracing;
+CREATE EXTENSION pg_tracing;
+SET pg_tracing.sample_rate = 0.0;
+-- View displaying spans with their nested level
+CREATE VIEW peek_spans_with_level AS
+    WITH RECURSIVE list_trace_spans(trace_id, parent_id, span_id, query_id, span_type, span_operation, span_start, span_end, sql_error_code, userid, dbid, pid, subxact_count, plan_startup_cost, plan_total_cost, plan_rows, plan_width, rows, nloops, shared_blks_hit, shared_blks_read, shared_blks_dirtied, shared_blks_written, local_blks_hit, local_blks_read, local_blks_dirtied, local_blks_written, blk_read_time, blk_write_time, temp_blks_read, temp_blks_written, temp_blk_read_time, temp_blk_write_time, wal_records, wal_fpi, wal_bytes, jit_functions, jit_generation_time, jit_inlining_time, jit_optimization_time, jit_emission_time, startup, parameters, lvl) AS (
+        SELECT p.*, 1
+        FROM pg_tracing_peek_spans p where not parent_id=ANY(SELECT span_id from pg_tracing_peek_spans)
+      UNION ALL
+        SELECT s.*, lvl + 1
+        FROM pg_tracing_peek_spans s, list_trace_spans st
+        WHERE s.parent_id = st.span_id
+    ) SELECT * FROM list_trace_spans;
+-- Create utility view to keep order stable
+CREATE VIEW peek_ordered_spans AS
+  SELECT * FROM peek_spans_with_level order by span_start, lvl, span_end;
+-- Nothing should have been generated
+select count(*) = 0 from pg_tracing_consume_spans;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Prepare and execute a prepared statement
+PREPARE test_prepared_one_param (integer) AS SELECT $1;
+EXECUTE test_prepared_one_param(100);
+ ?column? 
+----------
+      100
+(1 row)
+
+-- Nothing should be generated
+select count(*) = 0 from pg_tracing_consume_spans;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Force a query to start from ExecutorRun
+SET plan_cache_mode='force_generic_plan';
+EXECUTE test_prepared_one_param(200);
+ ?column? 
+----------
+      200
+(1 row)
+
+SET plan_cache_mode TO DEFAULT;
+-- Again, nothing should be generated
+select count(*) = 0 from pg_tracing_consume_spans;
+ ?column? 
+----------
+ t
+(1 row)
+
+--
+-- Test that no utility is captured with track_utility off
+--
+-- Enable utility tracking and track everything
+SET pg_tracing.track_utility = on;
+SET pg_tracing.sample_rate = 1.0;
+-- Prepare and execute a prepared statement
+PREPARE test_prepared_one_param_2 (integer) AS SELECT $1;
+EXECUTE test_prepared_one_param_2(100);
+ ?column? 
+----------
+      100
+(1 row)
+
+-- Check the number of generated spans
+select count(distinct(trace_id)) from pg_tracing_peek_spans;
+ count 
+-------
+     2
+(1 row)
+
+-- Check spans of test_prepared_one_param_2 execution
+select span_operation, parameters, lvl from peek_ordered_spans;
+                      span_operation                       | parameters | lvl 
+-----------------------------------------------------------+------------+-----
+ PREPARE test_prepared_one_param_2 (integer) AS SELECT $1; |            |   1
+ ProcessUtility                                            |            |   2
+ PREPARE test_prepared_one_param_2 (integer) AS SELECT $1; |            |   3
+ EXECUTE test_prepared_one_param_2(100);                   |            |   1
+ ProcessUtility                                            |            |   2
+ PREPARE test_prepared_one_param_2 (integer) AS SELECT $1; | $1 = '100' |   3
+ Planner                                                   |            |   4
+ ExecutorRun                                               |            |   4
+(8 rows)
+
+-- Check the top span (standalone top span has trace_id=parent_id)
+select span_operation, parameters, lvl from peek_ordered_spans where right(trace_id, 16) = parent_id;
+                      span_operation                       | parameters | lvl 
+-----------------------------------------------------------+------------+-----
+ PREPARE test_prepared_one_param_2 (integer) AS SELECT $1; |            |   1
+ EXECUTE test_prepared_one_param_2(100);                   |            |   1
+(2 rows)
+
+CALL clean_spans();
+-- Test prepared statement with generic plan
+SET plan_cache_mode='force_generic_plan';
+EXECUTE test_prepared_one_param(200);
+ ?column? 
+----------
+      200
+(1 row)
+
+SET plan_cache_mode TO DEFAULT;
+-- Check the number of generated spans
+select count(distinct(trace_id)) from pg_tracing_peek_spans;
+ count 
+-------
+     3
+(1 row)
+
+-- Check spans of test_prepared_one_param execution
+select span_operation, parameters, lvl from peek_ordered_spans;
+                     span_operation                      | parameters | lvl 
+---------------------------------------------------------+------------+-----
+ SET plan_cache_mode='force_generic_plan';               |            |   1
+ ProcessUtility                                          |            |   2
+ EXECUTE test_prepared_one_param(200);                   |            |   1
+ ProcessUtility                                          |            |   2
+ PREPARE test_prepared_one_param (integer) AS SELECT $1; |            |   3
+ ExecutorRun                                             |            |   4
+ SET plan_cache_mode TO DEFAULT;                         |            |   1
+ ProcessUtility                                          |            |   2
+(8 rows)
+
+-- Check the top span (standalone top span has trace_id=parent_id)
+select span_operation, parameters, lvl from peek_ordered_spans where right(trace_id, 16) = parent_id;
+              span_operation               | parameters | lvl 
+-------------------------------------------+------------+-----
+ SET plan_cache_mode='force_generic_plan'; |            |   1
+ EXECUTE test_prepared_one_param(200);     |            |   1
+ SET plan_cache_mode TO DEFAULT;           |            |   1
+(3 rows)
+
+CALL clean_spans();
+-- Second create extension should generate an error that is captured by span
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ CREATE EXTENSION pg_tracing;
+ERROR:  extension "pg_tracing" already exists
+select span_operation, parameters, sql_error_code, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000001';
+        span_operation        | parameters | sql_error_code | lvl 
+------------------------------+------------+----------------+-----
+ CREATE EXTENSION pg_tracing; |            | 42710          |   1
+ ProcessUtility               |            | 42710          |   2
+(2 rows)
+
+-- Create test table
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ CREATE TABLE pg_tracing_test (a int, b char(20));
+-- Check create table spans
+select trace_id, span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000002';
+             trace_id             |   span_type    |                  span_operation                   | lvl 
+----------------------------------+----------------+---------------------------------------------------+-----
+ 00000000000000000000000000000002 | Utility query  | CREATE TABLE pg_tracing_test (a int, b char(20)); |   1
+ 00000000000000000000000000000002 | ProcessUtility | ProcessUtility                                    |   2
+(2 rows)
+
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-01'*/ CREATE INDEX pg_tracing_index ON pg_tracing_test (a);
+-- Check create index spans
+select trace_id, span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000003';
+             trace_id             |   span_type    |                    span_operation                     | lvl 
+----------------------------------+----------------+-------------------------------------------------------+-----
+ 00000000000000000000000000000003 | Utility query  | CREATE INDEX pg_tracing_index ON pg_tracing_test (a); |   1
+ 00000000000000000000000000000003 | ProcessUtility | ProcessUtility                                        |   2
+(2 rows)
+
+CREATE OR REPLACE FUNCTION function_with_error(IN anyarray, OUT x anyelement, OUT n int)
+    RETURNS SETOF RECORD
+    LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE
+    AS 'select s from pg_catalog.generate_series(1, 1, 1) as g(s)';
+-- Check that tracing a function call with the wrong number of arguments is managed correctly
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000004-0000000000000004-01'*/ select function_with_error('{1,2,3}'::int[]);
+ERROR:  return type mismatch in function declared to return record
+DETAIL:  Final statement returns too few columns.
+CONTEXT:  SQL function "function_with_error" during startup
+-- Check lazy function call with error
+select trace_id, span_type, span_operation, sql_error_code, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000004';
+             trace_id             |  span_type   |                        span_operation                        | sql_error_code | lvl 
+----------------------------------+--------------+--------------------------------------------------------------+----------------+-----
+ 00000000000000000000000000000004 | Select query | select function_with_error($1::int[]);                       | 42P13          |   1
+ 00000000000000000000000000000004 | Planner      | Planner                                                      | 00000          |   2
+ 00000000000000000000000000000004 | Executor     | ExecutorRun                                                  | 42P13          |   2
+ 00000000000000000000000000000004 | Select query | select s from pg_catalog.generate_series($1, $2, $3) as g(s) | 00000          |   3
+(4 rows)
+
+-- Cleanup
+CALL clean_spans();
+SET pg_tracing.track_utility TO DEFAULT;
diff --git a/contrib/pg_tracing/expected/wal.out b/contrib/pg_tracing/expected/wal.out
new file mode 100644
index 0000000000..9e8fa02c31
--- /dev/null
+++ b/contrib/pg_tracing/expected/wal.out
@@ -0,0 +1,25 @@
+-- Generate queries with wal write
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ INSERT INTO pg_tracing_test VALUES(generate_series(1, 10), 'aaa');
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ UPDATE pg_tracing_test SET b = 'bbb' WHERE a = 7;
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-01'*/ DELETE FROM pg_tracing_test WHERE a = 9;
+-- Check WAL is generated for the above statements
+SELECT trace_id, span_type, span_operation,
+       wal_records > 0 as wal_records,
+       wal_bytes > 0 as wal_bytes
+FROM peek_ordered_spans;
+             trace_id             |  span_type   |                          span_operation                          | wal_records | wal_bytes 
+----------------------------------+--------------+------------------------------------------------------------------+-------------+-----------
+ 00000000000000000000000000000001 | Insert query | INSERT INTO pg_tracing_test VALUES(generate_series($1, $2), $3); | t           | t
+ 00000000000000000000000000000001 | Planner      | Planner                                                          | f           | f
+ 00000000000000000000000000000001 | Executor     | ExecutorRun                                                      |             | 
+ 00000000000000000000000000000002 | Update query | UPDATE pg_tracing_test SET b = $1 WHERE a = $2;                  | t           | t
+ 00000000000000000000000000000002 | Planner      | Planner                                                          | f           | f
+ 00000000000000000000000000000002 | Executor     | ExecutorRun                                                      |             | 
+ 00000000000000000000000000000003 | Delete query | DELETE FROM pg_tracing_test WHERE a = $1;                        | t           | t
+ 00000000000000000000000000000003 | Planner      | Planner                                                          | f           | f
+ 00000000000000000000000000000003 | Executor     | ExecutorRun                                                      |             | 
+(9 rows)
+
+CALL clean_spans();
+-- Cleanup
+CALL clean_spans();
diff --git a/contrib/pg_tracing/meson.build b/contrib/pg_tracing/meson.build
new file mode 100644
index 0000000000..8290a1a10e
--- /dev/null
+++ b/contrib/pg_tracing/meson.build
@@ -0,0 +1,53 @@
+# Copyright (c) 2022-2023, PostgreSQL Global Development Group
+
+pg_tracing_sources = files(
+  'pg_tracing.c',
+  'pg_tracing_query_process.c',
+  'pg_tracing_span.c',
+)
+
+if host_system == 'windows'
+  pg_tracing_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'pg_tracing',
+    '--FILEDESC', 'pg_tracing - Add support for distributed tracing',])
+endif
+
+pg_tracing = shared_module('pg_tracing',
+  pg_tracing_sources,
+  kwargs: contrib_mod_args + {
+    'dependencies': contrib_mod_args['dependencies'],
+  },
+)
+contrib_targets += pg_tracing
+
+install_data(
+  'pg_tracing.control',
+  'pg_tracing--1.0.sql',
+  kwargs: contrib_data_args,
+)
+
+tests += {
+  'name': 'pg_tracing',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'utility',
+      'select',
+      'extended',
+      'insert',
+      'trigger',
+      'sample',
+      'subxact',
+      'full_buffer',
+      'nested',
+      'wal',
+      'cleanup'
+      ],
+    'regress_args': ['--temp-config', files('pg_tracing.conf')],
+    # Disabled because these tests require
+    # "shared_preload_libraries=pg_tracing", which typical
+    # runningcheck users do not have (e.g. buildfarm clients).
+    'runningcheck': false,
+    },
+}
diff --git a/contrib/pg_tracing/pg_tracing--1.0.sql b/contrib/pg_tracing/pg_tracing--1.0.sql
new file mode 100644
index 0000000000..d400275c86
--- /dev/null
+++ b/contrib/pg_tracing/pg_tracing--1.0.sql
@@ -0,0 +1,101 @@
+/* contrib/pg_tracing/pg_tracing--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "CREATE EXTENSION pg_tracing" to load this file. \quit
+
+--- Define pg_tracing_info
+CREATE FUNCTION pg_tracing_info(
+    OUT processed_traces bigint,
+    OUT processed_spans bigint,
+    OUT dropped_traces bigint,
+    OUT dropped_spans bigint,
+    OUT last_consume timestamp with time zone,
+    OUT stats_reset timestamp with time zone
+)
+RETURNS record
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
+
+CREATE FUNCTION pg_tracing_reset()
+RETURNS void
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
+
+CREATE FUNCTION pg_tracing_spans(
+    IN consume bool,
+    OUT trace_id char(32),
+    OUT parent_id char(16),
+    OUT span_id char(16),
+    OUT query_id bigint,
+    OUT span_type text,
+    OUT span_operation text,
+    OUT span_start timestamp with time zone,
+    OUT span_end timestamp with time zone,
+    OUT sql_error_code character(5),
+    OUT pid int4,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT subxact_count smallint,
+
+--  Plan counters
+    OUT plan_startup_cost float8,
+    OUT plan_total_cost float8,
+    OUT plan_rows float8,
+    OUT plan_width int,
+
+-- Node Counters
+    OUT rows int8,
+    OUT nloops int8,
+
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+
+    OUT blk_read_time float8,
+    OUT blk_write_time float8,
+
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_time float8,
+
+--  Span node specific data
+    OUT startup bigint, -- First tuple
+    OUT parameters text
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT VOLATILE;
+
+CREATE VIEW pg_tracing_info AS
+  SELECT * FROM pg_tracing_info();
+
+CREATE VIEW pg_tracing_peek_spans AS
+  SELECT * FROM pg_tracing_spans(false);
+
+CREATE VIEW pg_tracing_consume_spans AS
+  SELECT * FROM pg_tracing_spans(true);
+
+GRANT SELECT ON pg_tracing_info TO PUBLIC;
+GRANT SELECT ON pg_tracing_peek_spans TO pg_read_all_stats;
+GRANT SELECT ON pg_tracing_consume_spans TO pg_read_all_stats;
+
+-- Don't want this to be available to non-superusers.
+REVOKE ALL ON FUNCTION pg_tracing_reset() FROM PUBLIC;
+REVOKE ALL ON FUNCTION pg_tracing_spans(boolean) FROM PUBLIC;
diff --git a/contrib/pg_tracing/pg_tracing.c b/contrib/pg_tracing/pg_tracing.c
new file mode 100644
index 0000000000..66e75e6438
--- /dev/null
+++ b/contrib/pg_tracing/pg_tracing.c
@@ -0,0 +1,2047 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_tracing.c
+ *		Generate spans for distributed tracing from SQL query
+ *
+ * Spans will only be generated for sampled queries. A query is sampled if:
+ * - It has a tracecontext propagated throught SQLCommenter and it passes the
+ *   caller_sample_rate.
+ * - It has no SQLCommenter but the query randomly passes the global sample_rate
+ *
+ * A query with SQLcommenter will look like: /\*dddbs='postgres.db',
+ * traceparent='00-00000000000000000000000000000009-0000000000000005-01'*\/ select 1;
+ * The traceparent fields are detailed in
+ * https://www.w3.org/TR/trace-context/#traceparent-header-field-values
+ * 00000000000000000000000000000009: trace id
+ * 0000000000000005: parent id
+ * 01: trace flags (01 == sampled)
+ *
+ * If sampled, pg_tracing will generate spans for the current statement. A span
+ * represents an operation with a start time, an end time and metadatas (block
+ * stats, wal stats...).
+ *
+ * We will track the following operations:
+ * - Top Span: The top span for a statement. They are created after extracting
+ *   the traceid from traceparent or to represent a nested query.
+ * - Planner: We track the time spent in the planner and report the planner counters.
+ * - Executor: We only track steps of the Executor that could contain nested queries:
+ *   Run and Finish
+ *
+ * A typical traced query will generate the following spans:
+ * +------------------------------------------------------+
+ * | Type: Select (top span)                              |
+ * | Operation: Select * pgbench_accounts WHERE aid=$1;   |
+ * +------------------------+---+-------------------------+
+ * | Type: Planner          |   |Type: Executor        |
+ * | Operation: Planner     |   |Operation: Run        |
+ * +------------------------+   +----------------------+
+ *
+ * At the end of a statement, all generated spans are copied in the shared memory.
+ * The spans are made available through the pg_tracing_peek_spans and
+ * pg_tracing_consume_spans. Sending spans to a trace endpoint is outside the scope of
+ * this module. It is expected that an external application will collect the spans
+ * through the provided pg_tracing_consume_spans view and send them to a trace consumer.
+ *
+ * IDENTIFICATION
+ *	  contrib/pg_tracing/pg_tracing.c
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "common/pg_prng.h"
+#include "funcapi.h"
+#include "nodes/extensible.h"
+#include "optimizer/planner.h"
+#include "parser/analyze.h"
+#include "pg_tracing.h"
+#include "storage/ipc.h"
+#include "tcop/utility.h"
+#include "utils/builtins.h"
+
+PG_MODULE_MAGIC;
+
+typedef enum
+{
+	PG_TRACING_TRACK_NONE,		/* track no statements */
+	PG_TRACING_TRACK_TOP,		/* only top level statements */
+	PG_TRACING_TRACK_ALL		/* all statements, including nested ones */
+}			pgTracingTrackLevel;
+
+typedef enum
+{
+	PG_TRACING_KEEP_ON_FULL,	/* Keep existing buffers when full */
+	PG_TRACING_DROP_ON_FULL		/* Drop current buffers when full */
+}			pgTracingBufferMode;
+
+/*
+ * Structure to store flexible array of spans
+ */
+typedef struct pgTracingSpans
+{
+	int			end;			/* Index of last element */
+	int			max;			/* Maximum number of element */
+	Span		spans[FLEXIBLE_ARRAY_MEMBER];
+}			pgTracingSpans;
+
+/*
+ * Structure to store per exec level informations
+ */
+typedef struct pgTracingPerLevelBuffer
+{
+	uint64		query_id;		/* Query id by for this level when available */
+	pgTracingSpans *top_spans;	/* top spans for the nested level */
+}			pgTracingPerLevelBuffer;
+
+/* GUC variables */
+static int	pg_tracing_max_span;	/* Maximum number of spans to store */
+static int	pg_tracing_max_parameter_str;	/* Maximum number of spans to
+											 * store */
+static double pg_tracing_sample_rate = 0;	/* Sample rate applied to queries
+											 * without SQLCommenter */
+static double pg_tracing_caller_sample_rate = 1;	/* Sample rate applied to
+													 * queries with
+													 * SQLCommenter */
+static bool pg_tracing_export_parameters = true;	/* Export query's
+													 * parameters as span
+													 * metadata */
+static int	pg_tracing_track = PG_TRACING_TRACK_ALL;	/* tracking level */
+static bool pg_tracing_track_utility = true;	/* whether to track utility
+												 * commands */
+static int	pg_tracing_buffer_mode = PG_TRACING_KEEP_ON_FULL;	/* behaviour on full
+																 * buffer */
+
+static const struct config_enum_entry track_options[] =
+{
+	{"none", PG_TRACING_TRACK_NONE, false},
+	{"top", PG_TRACING_TRACK_TOP, false},
+	{"all", PG_TRACING_TRACK_ALL, false},
+	{NULL, 0, false}
+};
+
+static const struct config_enum_entry buffer_mode_options[] =
+{
+	{"keep_on_full", PG_TRACING_KEEP_ON_FULL, false},
+	{"drop_on_full", PG_TRACING_DROP_ON_FULL, false},
+	{NULL, 0, false}
+};
+
+#define pg_tracking_level(level) \
+	((pg_tracing_track == PG_TRACING_TRACK_ALL || \
+	(pg_tracing_track == PG_TRACING_TRACK_TOP && (level) == 0)))
+
+#define pg_tracing_enabled(trace_context, level) \
+	(trace_context->traceparent.sampled && pg_tracking_level(level))
+
+#define INT64_HEX_FORMAT "%016" INT64_MODIFIER "x"
+
+PG_FUNCTION_INFO_V1(pg_tracing_info);
+PG_FUNCTION_INFO_V1(pg_tracing_spans);
+PG_FUNCTION_INFO_V1(pg_tracing_reset);
+
+/*
+ * Global variables
+ */
+
+/* Memory context for pg_tracing. */
+static MemoryContext pg_tracing_mem_ctx;
+
+/* trace context at the root level of parse/planning hook */
+static struct pgTracingTraceContext root_trace_context;
+
+/* trace context used in nested levels or within executor hooks */
+static struct pgTracingTraceContext current_trace_context;
+
+/* Latest trace id observed */
+static TraceId latest_trace_id;
+
+/* Latest local transaction id traced */
+static LocalTransactionId latest_lxid = InvalidLocalTransactionId;
+
+/* Shared state with stats and file external state */
+static pgTracingSharedState * pg_tracing = NULL;
+
+/*
+ * Shared buffer storing spans. Query with sampled flag will add new spans to the
+ * shared state at the end of the traced query.
+ * Those spans will be consumed during calls to pg_tracing_consume_spans.
+ */
+static pgTracingSpans * shared_spans = NULL;
+
+/*
+ * Store spans for the current trace.
+ * They will be added to shared_spans at the end of the query tracing.
+ */
+static pgTracingSpans * current_trace_spans;
+
+/*
+* Text for spans are buffered in this stringinfo and written at
+* the end of the query tracing in a single write.
+*/
+static StringInfo current_trace_text;
+
+/*
+ * Maximum nested level for a query to know how many top spans we need to
+ * copy in shared_spans.
+ */
+static int	max_nested_level = -1;
+
+/* Current nesting depth of planner calls */
+static int	plan_nested_level = 0;
+
+/* Current nesting depth of ExecutorRun+ProcessUtility calls */
+static int	exec_nested_level = 0;
+
+/* Number of spans initially allocated at the start of a trace. */
+static int	pg_tracing_initial_allocated_spans = 25;
+
+static pgTracingPerLevelBuffer * per_level_buffers = NULL;
+
+static void pg_tracing_shmem_request(void);
+static void pg_tracing_shmem_startup(void);
+
+/* Saved hook values in case of unload */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL;
+static planner_hook_type prev_planner_hook = NULL;
+static ExecutorStart_hook_type prev_ExecutorStart = NULL;
+static ExecutorRun_hook_type prev_ExecutorRun = NULL;
+static ExecutorFinish_hook_type prev_ExecutorFinish = NULL;
+static ExecutorEnd_hook_type prev_ExecutorEnd = NULL;
+static ProcessUtility_hook_type prev_ProcessUtility = NULL;
+
+static void pg_tracing_post_parse_analyze(ParseState *pstate, Query *query,
+										  JumbleState *jstate);
+static PlannedStmt *pg_tracing_planner_hook(Query *parse,
+											const char *query_string,
+											int cursorOptions,
+											ParamListInfo params);
+static void pg_tracing_ExecutorStart(QueryDesc *queryDesc, int eflags);
+static void pg_tracing_ExecutorRun(QueryDesc *queryDesc,
+								   ScanDirection direction,
+								   uint64 count, bool execute_once);
+static void pg_tracing_ExecutorFinish(QueryDesc *queryDesc);
+static void pg_tracing_ExecutorEnd(QueryDesc *queryDesc);
+static void pg_tracing_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
+									  bool readOnlyTree,
+									  ProcessUtilityContext context,
+									  ParamListInfo params,
+									  QueryEnvironment *queryEnv,
+									  DestReceiver *dest, QueryCompletion *qc);
+
+static pgTracingStats get_empty_pg_tracing_stats(void);
+
+static void cleanup_tracing(void);
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	if (!process_shared_preload_libraries_in_progress)
+		return;
+
+	DefineCustomIntVariable("pg_tracing.max_span",
+							"Maximum number of spans stored in shared memory.",
+							NULL,
+							&pg_tracing_max_span,
+							5000,
+							0,
+							500000,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+
+	DefineCustomIntVariable("pg_tracing.max_parameter_size",
+							"Maximum size of parameters. -1 to disable parameter in top span.",
+							NULL,
+							&pg_tracing_max_parameter_str,
+							1024,
+							0,
+							10000,
+							PGC_USERSET,
+							0,
+							NULL,
+							NULL,
+							NULL);
+
+	DefineCustomEnumVariable("pg_tracing.track",
+							 "Selects which statements are tracked by pg_tracing.",
+							 NULL,
+							 &pg_tracing_track,
+							 PG_TRACING_TRACK_ALL,
+							 track_options,
+							 PGC_USERSET,
+							 0,
+							 NULL,
+							 NULL,
+							 NULL);
+
+	DefineCustomBoolVariable("pg_tracing.track_utility",
+							 "Selects whether utility commands are tracked by pg_tracing.",
+							 NULL,
+							 &pg_tracing_track_utility,
+							 true,
+							 PGC_USERSET,
+							 0,
+							 NULL,
+							 NULL,
+							 NULL);
+
+	DefineCustomEnumVariable("pg_tracing.buffer_mode",
+							 "Controls behaviour on full buffer.",
+							 NULL,
+							 &pg_tracing_buffer_mode,
+							 PG_TRACING_KEEP_ON_FULL,
+							 buffer_mode_options,
+							 PGC_USERSET,
+							 0,
+							 NULL,
+							 NULL,
+							 NULL);
+
+	DefineCustomRealVariable("pg_tracing.sample_rate",
+							 "Fraction of queries without sampled flag or tracecontext to process.",
+							 NULL,
+							 &pg_tracing_sample_rate,
+							 0.0,
+							 0.0,
+							 1.0,
+							 PGC_USERSET,
+							 0,
+							 NULL,
+							 NULL,
+							 NULL);
+
+	DefineCustomRealVariable("pg_tracing.caller_sample_rate",
+							 "Fraction of queries having a tracecontext with sampled flag to process.",
+							 NULL,
+							 &pg_tracing_caller_sample_rate,
+							 1.0,
+							 0.0,
+							 1.0,
+							 PGC_USERSET,
+							 0,
+							 NULL,
+							 NULL,
+							 NULL);
+
+	DefineCustomBoolVariable("pg_tracing.export_parameters",
+							 "Export query's parameters as span metadata.",
+							 NULL,
+							 &pg_tracing_export_parameters,
+							 true,
+							 PGC_USERSET,
+							 0,
+							 NULL,
+							 NULL,
+							 NULL);
+
+	MarkGUCPrefixReserved("pg_tracing");
+
+	/* For jumble state */
+	EnableQueryId();
+
+	/* Install hooks. */
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = pg_tracing_shmem_request;
+
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = pg_tracing_shmem_startup;
+
+	prev_post_parse_analyze_hook = post_parse_analyze_hook;
+	post_parse_analyze_hook = pg_tracing_post_parse_analyze;
+
+	prev_planner_hook = planner_hook;
+	planner_hook = pg_tracing_planner_hook;
+
+	prev_ExecutorStart = ExecutorStart_hook;
+	ExecutorStart_hook = pg_tracing_ExecutorStart;
+	prev_ExecutorRun = ExecutorRun_hook;
+	ExecutorRun_hook = pg_tracing_ExecutorRun;
+	prev_ExecutorFinish = ExecutorFinish_hook;
+	ExecutorFinish_hook = pg_tracing_ExecutorFinish;
+	prev_ExecutorEnd = ExecutorEnd_hook;
+	ExecutorEnd_hook = pg_tracing_ExecutorEnd;
+
+	prev_ProcessUtility = ProcessUtility_hook;
+	ProcessUtility_hook = pg_tracing_ProcessUtility;
+}
+
+/*
+ * Estimate shared memory space needed.
+ */
+static Size
+pg_tracing_memsize(void)
+{
+	Size		size;
+
+	/* pg_tracing shared state */
+	size = sizeof(pgTracingSharedState);
+	/* span struct */
+	size = add_size(size, sizeof(pgTracingSpans));
+	/* the span variable array */
+	size = add_size(size, mul_size(pg_tracing_max_span, sizeof(Span)));
+
+	return size;
+}
+
+/*
+ * shmem_request hook: request additional shared resources.  We'll allocate
+ * or attach to the shared resources in pg_tracing_shmem_startup().
+ */
+static void
+pg_tracing_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+	RequestAddinShmemSpace(pg_tracing_memsize());
+	RequestNamedLWLockTranche("pg_tracing", 1);
+}
+
+/*
+ * Append a string current_trace_text StringInfo
+ */
+static int
+add_str_to_trace_buffer(const char *str, int str_len)
+{
+	int			position = current_trace_text->cursor;
+
+	Assert(str_len > 0);
+
+	appendBinaryStringInfo(current_trace_text, str, str_len);
+	appendStringInfoChar(current_trace_text, '\0');
+	current_trace_text->cursor = current_trace_text->len;
+	return position;
+}
+
+/*
+ * Store a span in the current_trace_spans buffer
+ */
+static void
+store_span(const Span * span)
+{
+	Assert(span->ended);
+	Assert(span->span_id > 0);
+
+	if (current_trace_spans->end >= current_trace_spans->max)
+	{
+		MemoryContext oldcxt;
+
+		/* Need to extend. */
+		current_trace_spans->max *= 2;
+		oldcxt = MemoryContextSwitchTo(pg_tracing_mem_ctx);
+		current_trace_spans = repalloc(current_trace_spans, sizeof(pgTracingSpans) +
+									   current_trace_spans->max * sizeof(Span));
+		MemoryContextSwitchTo(oldcxt);
+	}
+	current_trace_spans->spans[current_trace_spans->end++] = *span;
+}
+
+/*
+ * Get the latest span for a specific level. The span must exist
+ */
+static Span *
+get_latest_top_span(int nested_level)
+{
+	pgTracingSpans *top_spans;
+
+	Assert(nested_level <= max_nested_level);
+	Assert(nested_level >= 0);
+	top_spans = per_level_buffers[nested_level].top_spans;
+	Assert(top_spans->end > 0);
+	return &top_spans->spans[top_spans->end - 1];
+}
+
+/*
+ * Create a new top_span for the current exec nested level
+ */
+static Span *
+allocate_new_top_span(void)
+{
+	pgTracingSpans *top_spans;
+	Span	   *top_span;
+
+	top_spans = per_level_buffers[exec_nested_level].top_spans;
+	if (top_spans->end >= top_spans->max)
+	{
+		MemoryContext oldcxt;
+		int			old_spans_max = top_spans->max;
+
+		top_spans->max *= 2;
+		oldcxt = MemoryContextSwitchTo(pg_tracing_mem_ctx);
+		top_spans = repalloc0(top_spans,
+							  sizeof(pgTracingSpans) + old_spans_max * sizeof(Span),
+							  sizeof(pgTracingSpans) + old_spans_max * 2 * sizeof(Span));
+		per_level_buffers[exec_nested_level].top_spans = top_spans;
+		MemoryContextSwitchTo(oldcxt);
+	}
+	top_span = &top_spans->spans[top_spans->end++];
+	Assert(top_span->span_id == 0);
+	return top_span;
+}
+
+/*
+ * Drop the latest top span for the current nested level
+ */
+static void
+pop_top_span(void)
+{
+	pgTracingSpans *top_spans = per_level_buffers[exec_nested_level].top_spans;
+
+	Assert(top_spans->end > 0);
+	/* Reset span id of the discarded span since it could be reused */
+	top_spans->spans[--top_spans->end].span_id = 0;
+}
+
+/*
+ * End the latest top span for the current nested level.
+ * If pop_span is true, store the span in the current_trace_spans and remove it
+ * from the per level buffers.
+ */
+static void
+end_latest_top_span(const TimestampTz *end_time, bool pop_span)
+{
+	Span	   *top_span;
+
+	/* Check if the level was allocated */
+	if (exec_nested_level > max_nested_level)
+		return;
+
+	top_span = get_latest_top_span(exec_nested_level);
+	end_span(top_span, end_time);
+
+	/* Store span and remove it from the per_level_buffers */
+	if (pop_span)
+	{
+		store_span(top_span);
+		/* Restore previous top span */
+		pop_top_span();
+	}
+}
+
+/*
+ * End all spans for the current nested level
+ */
+static TimestampTz
+end_nested_level(void)
+{
+	TimestampTz span_end_time = GetCurrentTimestamp();
+	pgTracingSpans *top_spans;
+
+	if (exec_nested_level > max_nested_level)
+		/* No nested level were created */
+		goto finish;
+
+	top_spans = per_level_buffers[exec_nested_level].top_spans;
+	if (top_spans->end == 0)
+		/* No top spans to end */
+		goto finish;
+
+	for (int i = 0; i < top_spans->end; i++)
+	{
+		Span	   *top_span = top_spans->spans + i;
+
+		if (!top_span->ended)
+			end_span(top_span, &span_end_time);
+	}
+
+finish:
+	exec_nested_level--;
+	return span_end_time;
+}
+
+/*
+ * Drop all spans from the shared buffer and truncate the query file.
+ * Exclusive lock on pg_tracing->lock should be acquired beforehand.
+ */
+static void
+drop_all_spans_locked(void)
+{
+	/* Drop all spans */
+	shared_spans->end = 0;
+
+	/* Reset query file position */
+	pg_tracing->extent = 0;
+	pg_truncate(PG_TRACING_TEXT_FILE, 0);
+}
+
+/*
+ * Check if we still have available space in the shared spans.
+ *
+ * Between the moment we check and the moment we insert spans, the buffer may be
+ * full but we will redo a check before. This check is done when starting a top
+ * span to bail out early if the buffer is already full.
+ */
+static bool
+check_full_shared_spans(void)
+{
+	bool		full_buffer = false;
+
+	LWLockAcquire(pg_tracing->lock, LW_SHARED);
+	full_buffer = shared_spans->end >= shared_spans->max;
+	if (full_buffer && pg_tracing_buffer_mode != PG_TRACING_DROP_ON_FULL)
+	{
+		/*
+		 * Buffer is full and we want to keep existing spans. Drop the new
+		 * trace.
+		 */
+		pg_tracing->stats.dropped_traces++;
+		LWLockRelease(pg_tracing->lock);
+		return full_buffer;
+	}
+	LWLockRelease(pg_tracing->lock);
+
+	if (!full_buffer)
+		/* We have room in the shared buffer */
+		return full_buffer;
+
+	/*
+	 * We have a full buffer and we want to drop everything, get an exclusive
+	 * lock
+	 */
+	LWLockAcquire(pg_tracing->lock, LW_EXCLUSIVE);
+	/* Recheck for full buffer */
+	full_buffer = shared_spans->end >= shared_spans->max;
+	if (full_buffer)
+	{
+		pg_tracing->stats.dropped_spans += shared_spans->end;
+		drop_all_spans_locked();
+	}
+	LWLockRelease(pg_tracing->lock);
+
+	return full_buffer;
+}
+
+/*
+ * Reset trace_context fields
+ */
+static void
+reset_trace_context(pgTracingTraceContext * trace_context)
+{
+	trace_context->traceparent.sampled = 0;
+	trace_context->traceparent.trace_id.traceid_right = 0;
+	trace_context->traceparent.trace_id.traceid_left = 0;
+	trace_context->traceparent.parent_id = 0;
+	trace_context->root_span.span_id = 0;
+}
+
+/*
+ * shmem_startup hook: allocate or attach to shared memory, Also create and
+ * load the query-texts file, which is expected to exist (even if empty)
+ * while the module is enabled.
+ */
+static void
+pg_tracing_shmem_startup(void)
+{
+	bool		found_pg_tracing;
+	bool		found_shared_spans;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	/* reset in case this is a restart within the postmaster */
+	pg_tracing = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+
+	reset_trace_context(&root_trace_context);
+	reset_trace_context(&current_trace_context);
+	pg_tracing = ShmemInitStruct("PgTracing Shared", sizeof(pgTracingSharedState),
+								 &found_pg_tracing);
+	shared_spans = ShmemInitStruct("PgTracing Spans",
+								   sizeof(pgTracingSpans) +
+								   pg_tracing_max_span * sizeof(Span),
+								   &found_shared_spans);
+
+	/* Initialize pg_tracing memory context */
+	pg_tracing_mem_ctx = AllocSetContextCreate(TopMemoryContext,
+											   "pg_tracing memory context",
+											   ALLOCSET_DEFAULT_SIZES);
+
+	/* First time, let's init shared state */
+	if (!found_pg_tracing)
+	{
+		pg_tracing->stats = get_empty_pg_tracing_stats();
+		pg_tracing->lock = &(GetNamedLWLockTranche("pg_tracing"))->lock;
+	}
+	if (!found_shared_spans)
+	{
+		shared_spans->end = 0;
+		shared_spans->max = pg_tracing_max_span;
+	}
+	LWLockRelease(AddinShmemInitLock);
+}
+
+/*
+ * Process a query descriptor: Gather all query instrumentation in the query
+ * span counters
+ */
+static void
+process_query_desc(pgTracingTraceContext * trace_context, const QueryDesc *queryDesc,
+				   int sql_error_code, TimestampTz *end_time)
+{
+	NodeCounters *node_counters = &get_latest_top_span(exec_nested_level)->node_counters;
+
+	/* Process total counters */
+	if (queryDesc->totaltime)
+	{
+		InstrEndLoop(queryDesc->totaltime);
+		node_counters->buffer_usage = queryDesc->totaltime->bufusage;
+		node_counters->wal_usage = queryDesc->totaltime->walusage;
+	}
+	/* Process JIT counter */
+	if (queryDesc->estate->es_jit)
+		node_counters->jit_usage = queryDesc->estate->es_jit->instr;
+	node_counters->rows = queryDesc->estate->es_total_processed;
+}
+
+/*
+ * Create a trace id for the trace context if there's none.
+ * If trace was started from the global sample rate without a parent trace, we
+ * need to generate a random trace id.
+ */
+static void
+set_trace_id(struct pgTracingTraceContext *trace_context)
+{
+	if (!traceid_zero(trace_context->traceparent.trace_id))
+	{
+		/* Update last lxid seen */
+		latest_lxid = MyProc->vxid.lxid;
+		return;
+	}
+
+	/*
+	 * We want to keep the same trace id for all statements within the same
+	 * transaction. For that, we check if we're in the same local xid.
+	 */
+	if (MyProc->vxid.lxid == latest_lxid)
+	{
+		trace_context->traceparent.trace_id = latest_trace_id;
+		return;
+	}
+
+	/*
+	 * We leave parent_id to 0 as a way to indicate that this is a standalone
+	 * trace.
+	 */
+	Assert(trace_context->traceparent.parent_id == 0);
+
+	trace_context->traceparent.trace_id.traceid_left = pg_prng_int64(&pg_global_prng_state);
+	trace_context->traceparent.trace_id.traceid_right = pg_prng_int64(&pg_global_prng_state);
+	latest_trace_id = trace_context->traceparent.trace_id;
+	latest_lxid = MyProc->vxid.lxid;
+}
+
+/*
+ * Decide whether a query should be sampled depending on the traceparent sampled
+ * flag and the provided sample rate configurations
+ */
+static bool
+is_query_sampled(const struct pgTracingTraceContext *trace_context)
+{
+	double		rand;
+	bool		sampled;
+
+	/* Everything is sampled */
+	if (pg_tracing_sample_rate >= 1.0)
+		return true;
+
+	/* No SQLCommenter sampled and no global sample rate */
+	if (!trace_context->traceparent.sampled && pg_tracing_sample_rate == 0.0)
+		return false;
+
+	/* SQLCommenter sampled and caller sample is on */
+	if (trace_context->traceparent.sampled && pg_tracing_caller_sample_rate >= 1.0)
+		return true;
+
+	/*
+	 * We have a non zero global sample rate or a sampled flag. Either way, we
+	 * need a rand value
+	 */
+	rand = pg_prng_double(&pg_global_prng_state);
+	if (trace_context->traceparent.sampled)
+		/* Sampled flag case */
+		sampled = (rand < pg_tracing_caller_sample_rate);
+	else
+		/* Global rate case */
+		sampled = (rand < pg_tracing_sample_rate);
+	return sampled;
+}
+
+/*
+ * Extract trace_context from the query text.
+ * Sampling rate will be applied to the trace_context.
+ */
+static void
+extract_trace_context(struct pgTracingTraceContext *trace_context, ParseState *pstate,
+					  uint64 query_id)
+{
+	/* Timestamp of the latest statement checked for sampling. */
+	static TimestampTz last_statement_check_for_sampling = 0;
+	TimestampTz statement_start_ts;
+
+	/* Safety check... */
+	if (!pg_tracing || !pg_tracking_level(exec_nested_level))
+		return;
+
+	/* sampling already started */
+	if (trace_context->traceparent.sampled)
+		return;
+
+	/* Don't start tracing if we're not at the root level */
+	if (exec_nested_level > 0)
+		return;
+
+	/* Both sampling rate are set to 0, no tracing will happen */
+	if (pg_tracing_sample_rate == 0 && pg_tracing_caller_sample_rate == 0)
+		return;
+
+	Assert(trace_context->root_span.span_id == 0);
+	Assert(traceid_zero(trace_context->traceparent.trace_id));
+
+	if (pstate != NULL)
+		extract_trace_context_from_query(trace_context, pstate->p_sourcetext);
+
+	/*
+	 * A statement can go through this function 3 times: post parsing, planner
+	 * and executor run. If no sampling flag was extracted from SQLCommenter
+	 * and we've already seen this statement (using statement_start ts), don't
+	 * try to apply sample rate and exit.
+	 */
+	statement_start_ts = GetCurrentStatementStartTimestamp();
+	if (trace_context->traceparent.sampled == 0
+		&& last_statement_check_for_sampling == statement_start_ts)
+		return;
+	/* First time we see this statement, save the time */
+	last_statement_check_for_sampling = statement_start_ts;
+
+	trace_context->query_id = query_id;
+	trace_context->traceparent.sampled = is_query_sampled(trace_context);
+
+	if (trace_context->traceparent.sampled && check_full_shared_spans())
+		/* Buffer is full, abort sampling */
+		trace_context->traceparent.sampled = 0;
+
+	if (trace_context->traceparent.sampled)
+		set_trace_id(trace_context);
+	else
+		/* No sampling, reset the context */
+		reset_trace_context(trace_context);
+}
+
+/*
+ * Reset pg_tracing memory context and global state.
+ */
+static void
+cleanup_tracing(void)
+{
+	if (!root_trace_context.traceparent.sampled &&
+		!current_trace_context.traceparent.sampled)
+		/* No need for cleaning */
+		return;
+	MemoryContextReset(pg_tracing_mem_ctx);
+	reset_trace_context(&root_trace_context);
+	reset_trace_context(&current_trace_context);
+	max_nested_level = -1;
+	current_trace_spans = NULL;
+	per_level_buffers = NULL;
+}
+
+/*
+ * Add span to the shared memory.
+ * Exclusive lock on pg_tracing->lock must be acquired beforehand.
+ */
+static void
+add_span_to_shared_buffer_locked(const Span * span)
+{
+	/* Spans must be ended before adding them to the shared buffer */
+	Assert(span->ended);
+	if (shared_spans->end >= shared_spans->max)
+		pg_tracing->stats.dropped_spans++;
+	else
+	{
+		pg_tracing->stats.processed_spans++;
+		shared_spans->spans[shared_spans->end++] = *span;
+	}
+}
+
+/*
+ * End the query tracing and dump all spans in the shared buffer if we are at
+ * the root level. This may happen either when query is finished or on a caught error.
+ */
+static void
+end_tracing(pgTracingTraceContext * trace_context, Span * ongoing_span)
+{
+	Size		file_position = 0;
+
+	/* We're still a nested query, tracing is not finished */
+	if (exec_nested_level + plan_nested_level > 0)
+		return;
+
+	LWLockAcquire(pg_tracing->lock, LW_EXCLUSIVE);
+
+	if (current_trace_text->len > 0)
+	{
+		/* Dump all buffered texts in file */
+		text_store_file(pg_tracing, current_trace_text->data, current_trace_text->len,
+						&file_position);
+
+		/* Adjust file position of spans */
+		for (int i = 0; i < current_trace_spans->end; i++)
+			adjust_file_offset(current_trace_spans->spans + i, file_position);
+		for (int i = 0; i <= max_nested_level; i++)
+			for (int j = 0; j < per_level_buffers[i].top_spans->end; j++)
+				adjust_file_offset(per_level_buffers[i].top_spans->spans + j,
+								   file_position);
+		if (ongoing_span != NULL)
+			adjust_file_offset(ongoing_span, file_position);
+	}
+
+	/* We're at the end, add all stored spans to the shared memory */
+	for (int i = 0; i < current_trace_spans->end; i++)
+		add_span_to_shared_buffer_locked(&current_trace_spans->spans[i]);
+	/* And all top spans that were not pushed to current_trace_spans */
+	for (int i = 0; i <= max_nested_level; i++)
+		for (int j = 0; j < per_level_buffers[i].top_spans->end; j++)
+			add_span_to_shared_buffer_locked(&per_level_buffers[i].top_spans->spans[j]);
+
+	/*
+	 * We may have an ongoing span that was not allocated in
+	 * current_trace_spans nor per_level_buffers, add it to the shared spans
+	 */
+	if (ongoing_span != NULL)
+		add_span_to_shared_buffer_locked(ongoing_span);
+
+	/* Update our stats with the new trace */
+	pg_tracing->stats.processed_traces++;
+	LWLockRelease(pg_tracing->lock);
+
+	/* We can reset the memory context here */
+	cleanup_tracing();
+}
+
+/*
+ * When we catch an error (timeout, cancel query), we need to flag the ongoing
+ * span with an error, send current spans in the shared buffer and clean our
+ * memory context.
+ *
+ * We provide the ongoing span where the error was caught to attach the sql
+ * error code to it.
+ */
+static void
+handle_pg_error(pgTracingTraceContext * trace_context, Span * ongoing_span,
+				const QueryDesc *queryDesc,
+				TimestampTz span_end_time)
+{
+	int			sql_error_code;
+
+	/* If we're not sampling the query, bail out */
+	if (!pg_tracing_enabled(trace_context, exec_nested_level))
+		return;
+
+	sql_error_code = geterrcode();
+
+	if (queryDesc != NULL)
+		process_query_desc(trace_context, queryDesc, sql_error_code, &span_end_time);
+
+	/* End all ongoing top spans */
+	for (int i = 0; i <= max_nested_level; i++)
+	{
+		pgTracingSpans *top_spans = per_level_buffers[i].top_spans;
+
+		for (int j = 0; j < top_spans->end; j++)
+		{
+			Span	   *span = &top_spans->spans[j];
+
+			if (!span->ended)
+			{
+				/* Assign the error code to the latest top span */
+				span->sql_error_code = sql_error_code;
+				end_span(span, &span_end_time);
+			}
+		}
+	}
+
+	if (ongoing_span != NULL)
+	{
+		ongoing_span->sql_error_code = sql_error_code;
+		if (!ongoing_span->ended)
+			end_span(ongoing_span, &span_end_time);
+	}
+
+	end_tracing(trace_context, ongoing_span);
+}
+
+/*
+ * If we enter a new nested level, initialize all necessary buffers.
+ */
+static void
+initialize_trace_level(void)
+{
+	/* Number of allocated levels. */
+	static int	allocated_nested_level = 0;
+
+	Assert(exec_nested_level >= 0);
+
+	/* Check if we've already created a top span for this nested level */
+	if (exec_nested_level <= max_nested_level)
+		return;
+
+	/* First time */
+	if (max_nested_level == -1)
+	{
+		MemoryContext oldcxt;
+
+		Assert(pg_tracing_mem_ctx->isReset);
+
+		oldcxt = MemoryContextSwitchTo(pg_tracing_mem_ctx);
+		/* initial allocation */
+		allocated_nested_level = 1;
+		per_level_buffers = palloc0(allocated_nested_level *
+									sizeof(pgTracingPerLevelBuffer));
+		for (int i = 0; i < allocated_nested_level; i++)
+		{
+			per_level_buffers[i].top_spans = palloc0(sizeof(pgTracingSpans) +
+													 sizeof(Span));
+			per_level_buffers[i].top_spans->max = 1;
+		}
+		current_trace_spans = palloc0(sizeof(pgTracingSpans) +
+									  pg_tracing_initial_allocated_spans * sizeof(Span));
+		current_trace_spans->max = pg_tracing_initial_allocated_spans;
+		current_trace_text = makeStringInfo();
+
+		MemoryContextSwitchTo(oldcxt);
+	}
+	else if (exec_nested_level >= allocated_nested_level)
+	{
+		/* New nested level, allocate more memory */
+		MemoryContext oldcxt;
+		int			old_allocated_nested_level = allocated_nested_level;
+
+		oldcxt = MemoryContextSwitchTo(pg_tracing_mem_ctx);
+		allocated_nested_level++;
+		per_level_buffers = repalloc0(per_level_buffers, old_allocated_nested_level * sizeof(pgTracingPerLevelBuffer),
+									  allocated_nested_level * sizeof(pgTracingPerLevelBuffer));
+		for (int i = old_allocated_nested_level; i < allocated_nested_level; i++)
+		{
+			per_level_buffers[i].top_spans = palloc0(sizeof(pgTracingSpans) +
+													 sizeof(Span));
+			per_level_buffers[i].top_spans->max = 1;
+		}
+		MemoryContextSwitchTo(oldcxt);
+	}
+	max_nested_level = exec_nested_level;
+}
+
+/*
+ * Start a new top span if we've entered a new nested level or if the previous
+ * span at the same level ended.
+ */
+static void
+begin_top_span(pgTracingTraceContext * trace_context, Span * top_span,
+			   CmdType commandType, const Query *query, const JumbleState *jstate,
+			   const PlannedStmt *pstmt, const char *query_text, TimestampTz start_time)
+{
+	int			query_len;
+	const char *normalised_query;
+	uint64		parent_id;
+
+	/* in case of a cached plan, query might be unavailable */
+	if (query != NULL)
+		per_level_buffers[exec_nested_level].query_id = query->queryId;
+	else if (trace_context->query_id > 0)
+		per_level_buffers[exec_nested_level].query_id = trace_context->query_id;
+
+	if (exec_nested_level <= 0)
+		/* Root top span, use the parent id from the trace context */
+		parent_id = trace_context->traceparent.parent_id;
+	else
+		parent_id = get_latest_top_span(exec_nested_level - 1)->span_id;
+
+	begin_span(trace_context->traceparent.trace_id, top_span,
+			   command_type_to_span_type(commandType),
+			   NULL, parent_id,
+			   per_level_buffers[exec_nested_level].query_id, &start_time);
+
+	if (jstate && jstate->clocations_count > 0 && query != NULL)
+	{
+		/* jstate is available, normalise query and extract parameters' values */
+		char	   *param_str;
+		int			param_len;
+
+		query_len = query->stmt_len;
+		normalised_query = normalise_query_parameters(jstate, query_text,
+													  query->stmt_location, &query_len,
+													  &param_str, &param_len);
+		Assert(param_len > 0);
+		if (pg_tracing_export_parameters)
+			top_span->parameter_offset = add_str_to_trace_buffer(param_str, param_len);
+	}
+	else
+	{
+		/*
+		 * No jstate available, normalise query but we won't be able to
+		 * extract parameters
+		 */
+		int			stmt_location;
+
+		if (query != NULL && query->stmt_len > 0)
+		{
+			query_len = query->stmt_len;
+			stmt_location = query->stmt_location;
+		}
+		else if (pstmt != NULL && pstmt->stmt_location != -1 && pstmt->stmt_len > 0)
+		{
+			query_len = pstmt->stmt_len;
+			stmt_location = pstmt->stmt_location;
+		}
+		else
+		{
+			query_len = strlen(query_text);
+			stmt_location = 0;
+		}
+		normalised_query = normalise_query(query_text, stmt_location, &query_len);
+	}
+	top_span->operation_name_offset = add_str_to_trace_buffer(normalised_query,
+															  query_len);
+}
+
+/*
+ * Get the ongoing top span if it exists or create it
+ */
+static Span *
+get_or_allocate_top_span(pgTracingTraceContext * trace_context, bool in_parse_or_plan)
+{
+	Span	   *span;
+
+	if (in_parse_or_plan && exec_nested_level == 0)
+
+		/*
+		 * A the root post parse and plan, we want to use trace_context's
+		 * root_span as the top span in per_level_buffers might still be
+		 * ongoing.
+		 */
+		return &trace_context->root_span;
+
+	if (per_level_buffers[exec_nested_level].top_spans->end == 0)
+		/* No spans were created in this level, allocate a new one */
+		span = allocate_new_top_span();
+	else
+		span = get_latest_top_span(exec_nested_level);
+
+	if (exec_nested_level == 0)
+
+		/*
+		 * At root level and outside of parse/plan hook, we need to copy the
+		 * root span content
+		 */
+		*span = trace_context->root_span;
+
+	return span;
+}
+
+/*
+ * Initialise buffers if we are in a new nested level and start associated top span.
+ * If the top span already exists for the current nested level, this has no effect.
+ *
+ * This needs to be called every time a top span could be started: post parse,
+ * planner, executor start and process utility
+ */
+static uint64
+initialize_top_span(pgTracingTraceContext * trace_context, CmdType commandType,
+					Query *query, JumbleState *jstate, const PlannedStmt *pstmt,
+					const char *query_text, TimestampTz start_time,
+					bool in_parse_or_plan)
+{
+	Span	   *top_span;
+
+	/* Make sure we have allocated the level */
+	initialize_trace_level();
+
+	top_span = get_or_allocate_top_span(trace_context, in_parse_or_plan);
+
+	/* If the top_span is still ongoing, use it as it is */
+	if (top_span->span_id > 0 && top_span->ended == false)
+		return top_span->span_id;
+
+	/* current_trace_spans buffer should have been allocated */
+	Assert(current_trace_spans != NULL);
+
+	if (top_span->span_id > 0)
+	{
+		/* The previous top span was closed, create a new one */
+		Assert(top_span->ended);
+		top_span = allocate_new_top_span();
+	}
+
+	/* This is a new top span, start it */
+	begin_top_span(trace_context, top_span, commandType, query, jstate, pstmt,
+				   query_text, start_time);
+	return top_span->span_id;
+}
+
+/*
+ * Post-parse-analyze hook
+ *
+ * Tracing can be started here if:
+ * - The query has a SQLCommenter with traceparent parameter with sampled flag
+ *   on and passes the caller_sample_rate
+ * - The query passes the sample_rate
+ *
+ * If query is sampled, start the top span
+ */
+static void
+pg_tracing_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
+{
+	TimestampTz start_top_span;
+	pgTracingTraceContext *trace_context = &root_trace_context;
+	bool		new_lxid = MyProc->vxid.lxid != latest_lxid;
+
+	if (!pg_tracing_mem_ctx->isReset
+		&& new_lxid
+		&& exec_nested_level + plan_nested_level == 0)
+
+		/*
+		 * Some errors can happen outside of our PG_TRY (incorrect number of
+		 * bind parameters for example) which will leave a dirty trace
+		 * context. We can also have sampled individual Parse command through
+		 * extended protocol. In this case, we just drop the spans and reset
+		 * memory context
+		 */
+		cleanup_tracing();
+
+	if (exec_nested_level + plan_nested_level == 0)
+
+		/*
+		 * At the root level, clean any leftover state of previous trace
+		 * context
+		 */
+		reset_trace_context(&root_trace_context);
+	else
+		/* We're in a nested query, grab the ongoing trace_context */
+		trace_context = &current_trace_context;
+
+	if (prev_post_parse_analyze_hook)
+		prev_post_parse_analyze_hook(pstate, query, jstate);
+
+	/* If disabled, don't trace utility statement */
+	if (query->utilityStmt && !pg_tracing_track_utility)
+		return;
+
+	/* Evaluate if query is sampled or not */
+	extract_trace_context(trace_context, pstate, query->queryId);
+
+	if (!trace_context->traceparent.sampled)
+		/* Query is not sampled, nothing to do */
+		return;
+
+	/*
+	 * We want to avoid calling get_ns at the start of post parse as it will
+	 * impact all queries and we will only use it when the query is sampled.
+	 */
+	start_top_span = GetCurrentTimestamp();
+
+	/*
+	 * Either we're inside a nested sampled query or we've parsed a query with
+	 * the sampled flag, start a new level with a top span
+	 */
+	initialize_top_span(trace_context, query->commandType,
+						query, jstate, NULL,
+						pstate->p_sourcetext, start_top_span,
+						true);
+}
+
+/*
+ * Planner hook.
+ * Tracing can start here if the query skipped parsing (prepared statement) and
+ * passes the random sample_rate. If query is sampled, create a plan span and
+ * start the top span if not already started.
+ */
+static PlannedStmt *
+pg_tracing_planner_hook(Query *query, const char *query_string, int cursorOptions,
+						ParamListInfo params)
+{
+	PlannedStmt *result;
+	uint64		parent_id;
+	Span	   *span_planner;
+	pgTracingTraceContext *trace_context = &root_trace_context;
+	TimestampTz start_span_time;
+
+	if (exec_nested_level > 0)
+		/* We're in a nested query, grab the ongoing trace_context */
+		trace_context = &current_trace_context;
+
+	/* Evaluate if query is sampled or not */
+	extract_trace_context(trace_context, NULL, query->queryId);
+
+	if (!pg_tracing_enabled(trace_context, plan_nested_level + exec_nested_level))
+	{
+		/* No sampling */
+		if (prev_planner_hook)
+			result = prev_planner_hook(query, query_string, cursorOptions,
+									   params);
+		else
+			result = standard_planner(query, query_string, cursorOptions,
+									  params);
+		return result;
+	}
+
+	start_span_time = GetCurrentTimestamp();
+
+	if (plan_nested_level == 0)
+
+		/*
+		 * We may have skipped parsing if statement was prepared, create a new
+		 * top span in this case.
+		 */
+		parent_id = initialize_top_span(trace_context, query->commandType, query,
+										NULL, NULL, query_string, start_span_time, true);
+	else
+		/* We're in a nested plan, grab the latest top span */
+		parent_id = get_latest_top_span(exec_nested_level)->span_id;
+
+	/* Create and start the planner span */
+	span_planner = allocate_new_top_span();
+	begin_span(trace_context->traceparent.trace_id, span_planner, SPAN_PLANNER,
+			   NULL, parent_id,
+			   per_level_buffers[exec_nested_level].query_id, &start_span_time);
+
+	plan_nested_level++;
+	PG_TRY();
+	{
+		if (prev_planner_hook)
+			result = prev_planner_hook(query, query_string, cursorOptions, params);
+		else
+			result = standard_planner(query, query_string, cursorOptions, params);
+	}
+	PG_CATCH();
+	{
+		TimestampTz span_end_time = GetCurrentTimestamp();
+		Span	   *root_span = NULL;
+
+		plan_nested_level--;
+		span_planner->sql_error_code = geterrcode();
+		if (exec_nested_level == 0)
+		{
+			/*
+			 * The root span at level 0 is stored in trace_context. Ideally,
+			 * we should copy it to a top span created with
+			 * allocate_new_top_span(). However, since we are within a catch
+			 * block that could be due to a out of memory error, we want to
+			 * avoid new allocations. We can just pass the existing span to be
+			 * copied in the shared memory
+			 */
+			root_span = &trace_context->root_span;
+		}
+		handle_pg_error(trace_context, root_span, NULL, span_end_time);
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+	plan_nested_level--;
+
+	/* End planner span */
+	end_latest_top_span(NULL, true);
+
+	/* If we have a prepared statement, add bound parameters to the top span */
+	if (params != NULL && pg_tracing_export_parameters)
+	{
+		char	   *paramStr = BuildParamLogString(params, NULL,
+												   pg_tracing_max_parameter_str);
+
+		if (paramStr != NULL)
+		{
+			Span	   *top_span;
+
+			if (exec_nested_level == 0)
+				/* The root span at level 0 is stored in trace_context, use it */
+				top_span = &trace_context->root_span;
+			else
+				top_span = get_latest_top_span(exec_nested_level);
+			top_span->parameter_offset = add_str_to_trace_buffer(paramStr,
+																 strlen(paramStr));
+		}
+	}
+	return result;
+}
+
+/*
+ * ExecutorStart hook: Activate query instrumentation if query is sampled.
+ * Tracing can be started here if the query used a cached plan and passes the
+ * random sample_rate.
+ * If query is sampled, start the top span if it doesn't already exist.
+ */
+static void
+pg_tracing_ExecutorStart(QueryDesc *queryDesc, int eflags)
+{
+	pgTracingTraceContext *trace_context = &current_trace_context;
+	bool		executor_sampled;
+	bool		is_lazy_function;
+
+	if (exec_nested_level + plan_nested_level == 0)
+	{
+		/* We're at the root level, copy trace context from parsing/planning */
+		*trace_context = root_trace_context;
+	}
+
+	/* Evaluate if query is sampled or not */
+	extract_trace_context(trace_context, NULL, queryDesc->plannedstmt->queryId);
+
+	/*
+	 * We can detect the presence of lazy function through the node tag and
+	 * the executor flags. Lazy function will go through an ExecutorRun with
+	 * every call, possibly generating thousands of spans (ex: lazy call of
+	 * generate_series) which is not manageable and not very useful. If lazy
+	 * functions are detected, we don't generate spans.
+	 */
+	is_lazy_function = nodeTag(queryDesc->plannedstmt->planTree) == T_FunctionScan
+		&& eflags == EXEC_FLAG_SKIP_TRIGGERS;
+	executor_sampled = pg_tracing_enabled(trace_context, exec_nested_level)
+		&& !is_lazy_function;
+
+	if (executor_sampled)
+	{
+		TimestampTz start_span_time = GetCurrentTimestamp();
+
+		/*
+		 * In case of a cached plan, we haven't gone through neither parsing
+		 * nor planner hook. Create the top case in this case.
+		 */
+		initialize_top_span(trace_context, queryDesc->operation, NULL, NULL, NULL,
+							queryDesc->sourceText, start_span_time, false);
+	}
+
+	if (prev_ExecutorStart)
+		prev_ExecutorStart(queryDesc, eflags);
+	else
+		standard_ExecutorStart(queryDesc, eflags);
+
+	/* Allocate totaltime instrumentation in the per-query context */
+	if (executor_sampled && queryDesc->totaltime == NULL)
+	{
+		MemoryContext oldcxt;
+
+		oldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt);
+		queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false);
+		MemoryContextSwitchTo(oldcxt);
+	}
+}
+
+/*
+ * ExecutorRun hook: track nesting depth and create ExecutorRun span.
+ * ExecutorRun can create nested queries so we need to create ExecutorRun span
+ * as a top span.
+ */
+static void
+pg_tracing_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, uint64 count,
+					   bool execute_once)
+{
+	pgTracingTraceContext *trace_context = &current_trace_context;
+	TimestampTz span_end_time;
+	bool		executor_sampled = pg_tracing_enabled(trace_context, exec_nested_level) && queryDesc->totaltime != NULL;
+
+	if (executor_sampled)
+	{
+		Span	   *executor_run_span;
+		TimestampTz span_start_time = GetCurrentTimestamp();
+		uint64		parent_id = get_latest_top_span(exec_nested_level)->span_id;
+
+		/* Start ExecutorRun span as a new top span */
+		executor_run_span = allocate_new_top_span();
+		begin_span(trace_context->traceparent.trace_id, executor_run_span,
+				   SPAN_EXECUTOR_RUN, NULL, parent_id,
+				   per_level_buffers[exec_nested_level].query_id, &span_start_time);
+	}
+
+	exec_nested_level++;
+	PG_TRY();
+	{
+		if (prev_ExecutorRun)
+			prev_ExecutorRun(queryDesc, direction, count, execute_once);
+		else
+			standard_ExecutorRun(queryDesc, direction, count, execute_once);
+	}
+	PG_CATCH();
+	{
+		/*
+		 * When a query is executing the pg_tracing_{peek,consume}_spans
+		 * function, sampling will be disabled while we have an ongoing trace
+		 * and current_trace_spans will be NULL.
+		 */
+		if (current_trace_spans != NULL && executor_sampled)
+		{
+			span_end_time = end_nested_level();
+			handle_pg_error(trace_context, NULL, queryDesc, span_end_time);
+		}
+		else
+			exec_nested_level--;
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+
+	/*
+	 * Same as above, tracing could have been aborted, check for
+	 * current_trace_spans
+	 */
+	if (current_trace_spans != NULL && executor_sampled)
+	{
+		span_end_time = end_nested_level();
+		/* End ExecutorRun span and store it */
+		end_latest_top_span(&span_end_time, true);
+	}
+	else
+		exec_nested_level--;
+}
+
+/*
+ * ExecutorFinish hook: create executor finish span and track nesting depth.
+ * ExecutorFinish can start nested queries through triggers so we need
+ * to set the ExecutorFinish span as the top span.
+ */
+static void
+pg_tracing_ExecutorFinish(QueryDesc *queryDesc)
+{
+	pgTracingTraceContext *trace_context = &current_trace_context;
+	uint64		executor_finish_span_id;
+
+	TimestampTz span_end_time;
+	bool		executor_sampled = pg_tracing_enabled(trace_context, exec_nested_level) && queryDesc->totaltime != NULL;
+
+	if (executor_sampled)
+	{
+		TimestampTz span_start_time = GetCurrentTimestamp();
+		uint64		parent_id = get_latest_top_span(exec_nested_level)->span_id;
+
+		/* Create ExecutorFinish as a new potential top span */
+		Span	   *executor_finish_span = allocate_new_top_span();
+
+		begin_span(trace_context->traceparent.trace_id,
+				   executor_finish_span, SPAN_EXECUTOR_FINISH,
+				   NULL, parent_id,
+				   per_level_buffers[exec_nested_level].query_id, &span_start_time);
+		executor_finish_span_id = executor_finish_span->span_id;
+	}
+
+	exec_nested_level++;
+	PG_TRY();
+	{
+		if (prev_ExecutorFinish)
+			prev_ExecutorFinish(queryDesc);
+		else
+			standard_ExecutorFinish(queryDesc);
+	}
+	PG_CATCH();
+	{
+		if (current_trace_spans == NULL || !executor_sampled)
+			exec_nested_level--;
+		else
+		{
+			span_end_time = end_nested_level();
+			handle_pg_error(trace_context, NULL, queryDesc, span_end_time);
+		}
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+	if (current_trace_spans == NULL || !executor_sampled)
+	{
+		exec_nested_level--;
+		return;
+	}
+	span_end_time = end_nested_level();
+
+	/*
+	 * We only trace executorFinish when it has a nested query, check if we
+	 * have a possible child
+	 */
+	if (max_nested_level > exec_nested_level)
+	{
+		Span	   *nested_span = get_latest_top_span(exec_nested_level + 1);
+
+		/* Check if the child matches ExecutorFinish's id */
+		if (nested_span->parent_id != executor_finish_span_id)
+			/* No child for executor finish, discard it */
+			pop_top_span();
+		else
+			/* End ExecutorFinish span and store it */
+			end_latest_top_span(&span_end_time, true);
+	}
+	else
+		/* No child for ExecutorFinish, discard it */
+		pop_top_span();
+}
+
+/*
+ * ExecutorEnd hook will:
+ * - process queryDesc to extract informations from query instrumentation
+ * - end top span for the current nested level
+ * - end tracing if we're at the root level
+ */
+static void
+pg_tracing_ExecutorEnd(QueryDesc *queryDesc)
+{
+	pgTracingTraceContext *trace_context = &current_trace_context;
+	bool		executor_sampled = pg_tracing_enabled(trace_context, exec_nested_level) && queryDesc->totaltime != NULL;
+
+	if (executor_sampled)
+		process_query_desc(trace_context, queryDesc, 0, NULL);
+
+	/* No need to increment nested level here */
+	if (prev_ExecutorEnd)
+		prev_ExecutorEnd(queryDesc);
+	else
+		standard_ExecutorEnd(queryDesc);
+
+	if (executor_sampled)
+	{
+		TimestampTz span_end_time = GetCurrentTimestamp();
+		bool		overlapping_trace_context;
+		Span	   *top_span = get_latest_top_span(exec_nested_level);
+
+		/* End top span */
+		end_span(top_span, &span_end_time);
+
+		/*
+		 * if root trace context has a different root span index, it means
+		 * that we may have started to trace the next statement while still
+		 * processing the current one. This may happen with a transaction
+		 * block using extended protocol, the parsing of the next statement
+		 * happens before the ExecutorEnd of the current statement. In this
+		 * case, we can't end tracing as we still have unfinished spans.
+		 */
+		overlapping_trace_context = exec_nested_level == 0 &&
+			current_trace_context.root_span.span_id != root_trace_context.root_span.span_id &&
+			root_trace_context.traceparent.sampled;
+		if (overlapping_trace_context)
+			store_span(top_span);
+		else
+			end_tracing(trace_context, NULL);
+	}
+}
+
+/*
+ * ProcessUtility hook
+ *
+ * Trace utility query if utility tracking is enabled and sampling was enabled
+ * during parse step.
+ * Process utility may create nested queries (for example function CALL) so we need
+ * to set the ProcessUtility span as the top span before going through the standard
+ * codepath.
+ */
+static void
+pg_tracing_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
+						  bool readOnlyTree,
+						  ProcessUtilityContext context,
+						  ParamListInfo params, QueryEnvironment *queryEnv,
+						  DestReceiver *dest, QueryCompletion *qc)
+{
+	Span	   *process_utility_span;
+	uint64		parent_id;
+	TimestampTz span_end_time;
+	TimestampTz span_start_time;
+
+	/*
+	 * Save track utility value since this value could be modified by a SET
+	 * command
+	 */
+	bool		track_utility = pg_tracing_track_utility;
+	pgTracingTraceContext *trace_context = &current_trace_context;
+
+	if (exec_nested_level + plan_nested_level == 0)
+	{
+		/* We're at root level, copy the root trace_context */
+		*trace_context = root_trace_context;
+	}
+
+	if (!track_utility || !pg_tracing_enabled(trace_context, exec_nested_level))
+	{
+		/*
+		 * When utility tracking is disabled, we want to avoid tracing nested
+		 * queries created by the utility statement. Incrementing the nested
+		 * level will avoid starting a new nested trace while the root utility
+		 * wasn't traced
+		 */
+		if (!track_utility)
+			exec_nested_level++;
+
+		/* No sampling, just go through the standard process utility */
+		if (prev_ProcessUtility)
+			prev_ProcessUtility(pstmt, queryString, readOnlyTree,
+								context, params, queryEnv,
+								dest, qc);
+		else
+			standard_ProcessUtility(pstmt, queryString, readOnlyTree,
+									context, params, queryEnv,
+									dest, qc);
+		if (!track_utility)
+			exec_nested_level--;
+
+		/*
+		 * Tracing may have been started within the utility call without going
+		 * through ExecutorEnd (ex: prepare statement). Check and end tracing
+		 * here in this case.
+		 */
+		if (!pg_tracing_mem_ctx->isReset && exec_nested_level == 0)
+		{
+			Assert(current_trace_context.traceparent.sampled ||
+				   root_trace_context.traceparent.sampled);
+			end_tracing(trace_context, NULL);
+		}
+		return;
+	}
+
+	/* Statement is sampled */
+	span_start_time = GetCurrentTimestamp();
+
+	parent_id = initialize_top_span(trace_context, pstmt->commandType, NULL,
+									NULL, pstmt, queryString, span_start_time, false);
+	process_utility_span = allocate_new_top_span();
+
+	/* Build the process utility span. */
+	begin_span(trace_context->traceparent.trace_id, process_utility_span,
+			   SPAN_PROCESS_UTILITY, NULL, parent_id,
+			   per_level_buffers[exec_nested_level].query_id, &span_start_time);
+
+	exec_nested_level++;
+	PG_TRY();
+	{
+		if (prev_ProcessUtility)
+			prev_ProcessUtility(pstmt, queryString, readOnlyTree, context,
+								params, queryEnv, dest, qc);
+		else
+			standard_ProcessUtility(pstmt, queryString, readOnlyTree, context,
+									params, queryEnv, dest, qc);
+	}
+	PG_CATCH();
+	{
+		/*
+		 * Tracing may have been reset within processUtility, check for
+		 * current_trace_spans
+		 */
+		if (current_trace_spans != NULL)
+		{
+			span_end_time = end_nested_level();
+			handle_pg_error(trace_context, NULL, NULL, span_end_time);
+		}
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+
+	/* Same as above, abort if tracing was disabled within process utility */
+	if (current_trace_spans == NULL)
+	{
+		exec_nested_level--;
+		return;
+	}
+	span_end_time = end_nested_level();
+
+	/* buffer may have been repalloced, grab a fresh pointer */
+	process_utility_span = get_latest_top_span(exec_nested_level);
+	if (qc != NULL)
+		process_utility_span->node_counters.rows = qc->nprocessed;
+
+	/* End ProcessUtility span and store it */
+	end_latest_top_span(&span_end_time, true);
+
+	/*
+	 * We're at a potential end for the query, end the parent top span that
+	 * was restored and end tracing
+	 */
+	end_latest_top_span(&span_end_time, false);
+	end_tracing(trace_context, NULL);
+}
+
+/*
+ * Add plan counters to the Datum output
+ */
+static int
+add_plan_counters(const PlanCounters * plan_counters, int i, Datum *values)
+{
+	values[i++] = Float8GetDatumFast(plan_counters->startup_cost);
+	values[i++] = Float8GetDatumFast(plan_counters->total_cost);
+	values[i++] = Float8GetDatumFast(plan_counters->plan_rows);
+	values[i++] = Int32GetDatum(plan_counters->plan_width);
+	return i;
+}
+
+/*
+ * Add node counters to the Datum output
+ */
+static int
+add_node_counters(const NodeCounters * node_counters, int i, Datum *values)
+{
+	Datum		wal_bytes;
+	char		buf[256];
+	double		blk_read_time,
+				blk_write_time,
+				temp_blk_read_time,
+				temp_blk_write_time;
+	double		generation_counter,
+				inlining_counter,
+				optimization_counter,
+				emission_counter;
+	int64		jit_created_functions;
+
+	values[i++] = Int64GetDatumFast(node_counters->rows);
+	values[i++] = Int64GetDatumFast(node_counters->nloops);
+
+	/* Buffer usage */
+	values[i++] = Int64GetDatumFast(node_counters->buffer_usage.shared_blks_hit);
+	values[i++] = Int64GetDatumFast(node_counters->buffer_usage.shared_blks_read);
+	values[i++] = Int64GetDatumFast(node_counters->buffer_usage.shared_blks_dirtied);
+	values[i++] = Int64GetDatumFast(node_counters->buffer_usage.shared_blks_written);
+
+	values[i++] = Int64GetDatumFast(node_counters->buffer_usage.local_blks_hit);
+	values[i++] = Int64GetDatumFast(node_counters->buffer_usage.local_blks_read);
+	values[i++] = Int64GetDatumFast(node_counters->buffer_usage.local_blks_dirtied);
+	values[i++] = Int64GetDatumFast(node_counters->buffer_usage.local_blks_written);
+
+	blk_read_time = INSTR_TIME_GET_MILLISEC(node_counters->buffer_usage.shared_blk_read_time);
+	blk_write_time = INSTR_TIME_GET_MILLISEC(node_counters->buffer_usage.shared_blk_write_time);
+	temp_blk_read_time = INSTR_TIME_GET_MILLISEC(node_counters->buffer_usage.temp_blk_read_time);
+	temp_blk_write_time = INSTR_TIME_GET_MILLISEC(node_counters->buffer_usage.temp_blk_write_time);
+
+	values[i++] = Float8GetDatumFast(blk_read_time);
+	values[i++] = Float8GetDatumFast(blk_write_time);
+	values[i++] = Float8GetDatumFast(temp_blk_read_time);
+	values[i++] = Float8GetDatumFast(temp_blk_write_time);
+
+	values[i++] = Int64GetDatumFast(node_counters->buffer_usage.temp_blks_read);
+	values[i++] = Int64GetDatumFast(node_counters->buffer_usage.temp_blks_written);
+
+	/* WAL usage */
+	values[i++] = Int64GetDatumFast(node_counters->wal_usage.wal_records);
+	values[i++] = Int64GetDatumFast(node_counters->wal_usage.wal_fpi);
+	snprintf(buf, sizeof buf, UINT64_FORMAT, node_counters->wal_usage.wal_bytes);
+
+	/* Convert to numeric. */
+	wal_bytes = DirectFunctionCall3(numeric_in,
+									CStringGetDatum(buf),
+									ObjectIdGetDatum(0),
+									Int32GetDatum(-1));
+	values[i++] = wal_bytes;
+
+	/* JIT usage */
+	generation_counter = INSTR_TIME_GET_MILLISEC(node_counters->jit_usage.generation_counter);
+	inlining_counter = INSTR_TIME_GET_MILLISEC(node_counters->jit_usage.inlining_counter);
+	optimization_counter = INSTR_TIME_GET_MILLISEC(node_counters->jit_usage.optimization_counter);
+	emission_counter = INSTR_TIME_GET_MILLISEC(node_counters->jit_usage.emission_counter);
+	jit_created_functions = node_counters->jit_usage.created_functions;
+
+	values[i++] = Int64GetDatumFast(jit_created_functions);
+	values[i++] = Float8GetDatumFast(generation_counter);
+	values[i++] = Float8GetDatumFast(inlining_counter);
+	values[i++] = Float8GetDatumFast(optimization_counter);
+	values[i++] = Float8GetDatumFast(emission_counter);
+
+	return i;
+}
+
+/*
+ * Build the tuple for a Span and add it to the output
+ */
+static void
+add_result_span(ReturnSetInfo *rsinfo, Span * span,
+				const char *qbuffer, Size qbuffer_size)
+{
+#define PG_TRACING_TRACES_COLS	43
+	Datum		values[PG_TRACING_TRACES_COLS] = {0};
+	bool		nulls[PG_TRACING_TRACES_COLS] = {0};
+	const char *span_type;
+	const char *operation_name;
+	const char *sql_error_code;
+	int			i = 0;
+	char		trace_id[33];
+	char		parent_id[17];
+	char		span_id[17];
+
+	span_type = get_span_type(span, qbuffer, qbuffer_size);
+	operation_name = get_operation_name(span, qbuffer, qbuffer_size);
+	sql_error_code = unpack_sql_state(span->sql_error_code);
+
+	pg_snprintf(trace_id, 33, INT64_HEX_FORMAT INT64_HEX_FORMAT,
+				span->trace_id.traceid_left,
+				span->trace_id.traceid_right);
+	pg_snprintf(parent_id, 17, INT64_HEX_FORMAT, span->parent_id);
+	pg_snprintf(span_id, 17, INT64_HEX_FORMAT, span->span_id);
+
+	Assert(span_type != NULL);
+	Assert(operation_name != NULL);
+	Assert(sql_error_code != NULL);
+
+	values[i++] = CStringGetTextDatum(trace_id);
+	values[i++] = CStringGetTextDatum(parent_id);
+	values[i++] = CStringGetTextDatum(span_id);
+	values[i++] = UInt64GetDatum(span->query_id);
+	values[i++] = CStringGetTextDatum(span_type);
+	values[i++] = CStringGetTextDatum(operation_name);
+	values[i++] = Int64GetDatumFast(span->start);
+	values[i++] = Int64GetDatumFast(span->end);
+
+	values[i++] = CStringGetTextDatum(sql_error_code);
+	values[i++] = UInt32GetDatum(span->be_pid);
+	values[i++] = ObjectIdGetDatum(span->user_id);
+	values[i++] = ObjectIdGetDatum(span->database_id);
+	values[i++] = UInt8GetDatum(span->subxact_count);
+
+	/* Only node and top spans have counters */
+	if ((span->type >= SPAN_TOP_SELECT && span->type <= SPAN_TOP_UNKNOWN)
+		|| span->type == SPAN_PLANNER)
+	{
+		i = add_plan_counters(&span->plan_counters, i, values);
+		i = add_node_counters(&span->node_counters, i, values);
+
+		values[i++] = Int64GetDatumFast(span->startup);
+		if (span->parameter_offset != -1 && qbuffer_size > 0 && qbuffer_size > span->parameter_offset)
+			values[i++] = CStringGetTextDatum(qbuffer + span->parameter_offset);
+		else
+			nulls[i++] = 1;
+	}
+
+	for (int j = i; j < PG_TRACING_TRACES_COLS; j++)
+		nulls[j] = 1;
+
+	tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+}
+
+/*
+ * Return spans as a result set.
+ *
+ * Accept a consume parameter. When consume is set,
+ * we empty the shared buffer and truncate query text.
+ */
+Datum
+pg_tracing_spans(PG_FUNCTION_ARGS)
+{
+	bool		consume;
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	Span	   *span;
+	const char *qbuffer;
+	Size		qbuffer_size = 0;
+	LWLockMode	lock_mode = LW_SHARED;
+
+	consume = PG_GETARG_BOOL(0);
+
+	/*
+	 * We need an exclusive lock to truncate and empty the shared buffer when
+	 * we consume
+	 */
+	if (consume)
+		lock_mode = LW_EXCLUSIVE;
+
+	if (!pg_tracing)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("pg_tracing must be loaded via shared_preload_libraries")));
+	InitMaterializedSRF(fcinfo, 0);
+
+	/*
+	 * If this query was sampled and we're consuming tracing_spans buffer, the
+	 * spans will target a query string that doesn't exist anymore in the
+	 * query file. Better abort the sampling and clean ongoing traces. Since
+	 * this will be called within an ExecutorRun, we will need to check for
+	 * current_trace_spans at the end of the ExecutorRun hook.
+	 */
+	cleanup_tracing();
+
+	qbuffer = qtext_load_file(&qbuffer_size);
+	if (qbuffer == NULL)
+
+		/*
+		 * It's possible to get NULL if file was truncated while we read it.
+		 * Abort in this case.
+		 */
+		return (Datum) 0;
+
+	LWLockAcquire(pg_tracing->lock, lock_mode);
+	for (int i = 0; i < shared_spans->end; i++)
+	{
+		span = shared_spans->spans + i;
+		add_result_span(rsinfo, span, qbuffer, qbuffer_size);
+	}
+
+	/* Consume is set, remove spans from the shared buffer */
+	if (consume)
+		drop_all_spans_locked();
+	pg_tracing->stats.last_consume = GetCurrentTimestamp();
+	LWLockRelease(pg_tracing->lock);
+
+	return (Datum) 0;
+}
+
+/*
+ * Return statistics of pg_tracing.
+ */
+Datum
+pg_tracing_info(PG_FUNCTION_ARGS)
+{
+#define PG_TRACING_INFO_COLS	6
+	pgTracingStats stats;
+	TupleDesc	tupdesc;
+	Datum		values[PG_TRACING_INFO_COLS] = {0};
+	bool		nulls[PG_TRACING_INFO_COLS] = {0};
+	int			i = 0;
+
+	if (!pg_tracing)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("pg_tracing must be loaded via shared_preload_libraries")));
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Get a copy of the pg_tracing stats */
+	LWLockAcquire(pg_tracing->lock, LW_SHARED);
+	stats = pg_tracing->stats;
+	LWLockRelease(pg_tracing->lock);
+
+	values[i++] = Int64GetDatum(stats.processed_traces);
+	values[i++] = Int64GetDatum(stats.processed_spans);
+	values[i++] = Int64GetDatum(stats.dropped_traces);
+	values[i++] = Int64GetDatum(stats.dropped_spans);
+	values[i++] = TimestampTzGetDatum(stats.last_consume);
+	values[i++] = TimestampTzGetDatum(stats.stats_reset);
+
+	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
+/*
+ * Get an empty pgTracingStats
+ */
+static pgTracingStats
+get_empty_pg_tracing_stats(void)
+{
+	pgTracingStats stats;
+
+	stats.processed_traces = 0;
+	stats.processed_spans = 0;
+	stats.dropped_traces = 0;
+	stats.dropped_spans = 0;
+	stats.last_consume = 0;
+	stats.stats_reset = GetCurrentTimestamp();
+	return stats;
+}
+
+/*
+ * Reset pg_tracing statistics.
+ */
+Datum
+pg_tracing_reset(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Reset statistics for pg_tracing since all entries are removed.
+	 */
+	pgTracingStats empty_stats = get_empty_pg_tracing_stats();
+
+	LWLockAcquire(pg_tracing->lock, LW_EXCLUSIVE);
+	pg_tracing->stats = empty_stats;
+	LWLockRelease(pg_tracing->lock);
+
+	PG_RETURN_VOID();
+}
diff --git a/contrib/pg_tracing/pg_tracing.conf b/contrib/pg_tracing/pg_tracing.conf
new file mode 100644
index 0000000000..ab40cd2053
--- /dev/null
+++ b/contrib/pg_tracing/pg_tracing.conf
@@ -0,0 +1,2 @@
+shared_preload_libraries = 'pg_tracing'
+pg_tracing.max_span = 40
diff --git a/contrib/pg_tracing/pg_tracing.control b/contrib/pg_tracing/pg_tracing.control
new file mode 100644
index 0000000000..efa78a6610
--- /dev/null
+++ b/contrib/pg_tracing/pg_tracing.control
@@ -0,0 +1,5 @@
+# pg_tracing extension
+comment = 'Distributed tracing for postgres'
+default_version = '1.0'
+module_pathname = '$libdir/pg_tracing'
+relocatable = true
diff --git a/contrib/pg_tracing/pg_tracing.h b/contrib/pg_tracing/pg_tracing.h
new file mode 100644
index 0000000000..7a871c19f0
--- /dev/null
+++ b/contrib/pg_tracing/pg_tracing.h
@@ -0,0 +1,196 @@
+/*-------------------------------------------------------------------------
+ * pg_tracing.h
+ * 		Header for pg_tracing.
+ *
+ * IDENTIFICATION
+ *	  contrib/pg_tracing/pg_tracing.h
+ *-------------------------------------------------------------------------
+ */
+#ifndef _PG_TRACING_H_
+#define _PG_TRACING_H_
+
+#include "executor/execdesc.h"
+#include "nodes/queryjumble.h"
+#include "jit/jit.h"
+#include "pgstat.h"
+#include "storage/lwlock.h"
+
+/* Location of external text file */
+#define PG_TRACING_TEXT_FILE	PG_STAT_TMP_DIR "/pg_tracing.stat"
+
+/*
+ * Counters extracted from query instrumentation
+ */
+typedef struct NodeCounters
+{
+	int64		rows;			/* # of tuples processed */
+	int64		nloops;			/* # of cycles for this node */
+	BufferUsage buffer_usage;	/* total buffer usage for this node */
+	WalUsage	wal_usage;		/* total WAL usage for this node */
+	JitInstrumentation jit_usage;	/* total JIT usage for this node */
+}			NodeCounters;
+
+/*
+ * Counters extracted from query's plan
+ */
+typedef struct PlanCounters
+{
+	/* estimated execution costs for plan (see costsize.c for more info) */
+	Cost		startup_cost;	/* cost expended before fetching any tuples */
+	Cost		total_cost;		/* total cost (assuming all tuples fetched) */
+
+	/* planner's estimate of result size of this plan step */
+	Cardinality plan_rows;		/* number of rows plan is expected to emit */
+	int			plan_width;		/* average row width in bytes */
+}			PlanCounters;
+
+/*
+ * SpanType: Type of the span
+ */
+typedef enum SpanType
+{
+	SPAN_PLANNER,				/* Wraps planner execution in planner hook */
+	SPAN_FUNCTION,				/* Wraps function in fmgr hook */
+	SPAN_PROCESS_UTILITY,		/* Wraps ProcessUtility execution */
+
+	SPAN_EXECUTOR_RUN,			/* Wraps Executor run hook */
+	SPAN_EXECUTOR_FINISH,		/* Wraps Executor finish hook */
+
+	/* Top Span types. They are created from the query cmdType */
+	SPAN_TOP_SELECT,
+	SPAN_TOP_INSERT,
+	SPAN_TOP_UPDATE,
+	SPAN_TOP_DELETE,
+	SPAN_TOP_MERGE,
+	SPAN_TOP_UTILITY,
+	SPAN_TOP_NOTHING,
+	SPAN_TOP_UNKNOWN,
+}			SpanType;
+
+
+/*
+ * In standard, traceId is defined as a 16 bytes array propagated
+ * with 32 hexadecimal characters. We store it as 2 int64 for simplicity
+ * and to reduce memory footprint.
+*/
+typedef struct
+{
+	uint64		traceid_left;
+	uint64		traceid_right;
+}			TraceId;
+
+
+/*
+ * The Span data structure represents an operation with a start, a duration
+ * and metadatas.
+ */
+typedef struct Span
+{
+	TraceId		trace_id;		/* Trace id extracted from the SQLCommenter's
+								 * traceparent */
+	uint64		span_id;		/* Span Identifier generated from a random
+								 * uint64 */
+	uint64		parent_id;		/* Span's parent id. For the top span, it's
+								 * extracted from SQLCommenter's traceparent.
+								 * For other spans, we pass the parent's span */
+	uint64		query_id;		/* QueryId of the trace query if available */
+
+	TimestampTz start;			/* Start of the span */
+	TimestampTz end;			/* End of the span */
+
+	SpanType	type;			/* Type of the span. Used to generate the
+								 * span's name */
+	bool		ended;			/* Track if the span was already ended.
+								 * Internal usage only */
+	int			be_pid;			/* Pid of the backend process */
+	Oid			user_id;		/* User ID when the span was created */
+	Oid			database_id;	/* Database ID where the span was created */
+
+	uint8		subxact_count;	/* Active count of backend's subtransaction */
+
+	/*
+	 * We store variable size metadata in an external file. Those represent
+	 * the position of NULL terminated strings in the file. Set to -1 if
+	 * unused.
+	 */
+	Size		node_type_offset;	/* name of the node type */
+	Size		operation_name_offset;	/* operation represented by the span */
+	Size		parameter_offset;	/* query parameters values */
+
+	PlanCounters plan_counters; /* Counters with plan costs */
+	NodeCounters node_counters; /* Counters with node costs (jit, wal,
+								 * buffers) */
+	int64		startup;		/* Time to the first tuple in nanoseconds */
+	int			sql_error_code; /* query error code extracted from ErrorData,
+								 * 0 if query was successful */
+}			Span;
+
+
+/*
+ * Global statistics for pg_tracing
+ */
+typedef struct pgTracingStats
+{
+	int64		processed_traces;	/* number of traces processed */
+	int64		processed_spans;	/* number of spans processed */
+	int64		dropped_traces; /* number of traces aborted due to full buffer */
+	int64		dropped_spans;	/* number of dropped spans due to full buffer */
+	TimestampTz last_consume;	/* Last time the shared spans buffer was
+								 * consumed */
+	TimestampTz stats_reset;	/* Last time stats were reset */
+}			pgTracingStats;
+
+/*
+ * Global shared state
+ */
+typedef struct pgTracingSharedState
+{
+	LWLock	   *lock;			/* protects shared spans, shared state and
+								 * query file */
+	Size		extent;			/* current extent of query file */
+	pgTracingStats stats;		/* global statistics for pg_tracing */
+}			pgTracingSharedState;
+
+/*
+ * Traceparent values propagated by the caller
+ */
+typedef struct pgTracingTraceparent
+{
+	TraceId		trace_id;		/* Id of the trace */
+	uint64		parent_id;		/* Span id of the parent */
+	int			sampled;		/* Is current statement sampled? */
+}			pgTracingTraceparent;
+
+/*
+ * Trace context for an ongoing trace
+ */
+typedef struct pgTracingTraceContext
+{
+	pgTracingTraceparent traceparent;
+	uint64		query_id;		/* Query id of the current statement */
+	Span		root_span;		/* Top span for exec_nested_level 0 */
+}			pgTracingTraceContext;
+
+/* pg_tracing_query_process.c */
+extern const char *normalise_query_parameters(const JumbleState *jstate, const char *query,
+											  int query_loc, int *query_len_p, char **param_str,
+											  int *param_len);
+extern void extract_trace_context_from_query(pgTracingTraceContext * trace_context, const char *query);
+extern void parse_trace_context(pgTracingTraceContext * trace_context, const char *trace_context_str, int trace_context_len);
+extern const char *normalise_query(const char *query, int query_loc, int *query_len_p);
+extern bool text_store_file(pgTracingSharedState * pg_tracing, const char *query,
+							int query_len, Size *query_offset);
+extern const char *qtext_load_file(Size *buffer_size);
+extern const char *qtext_load_file(Size *buffer_size);
+
+/* pg_tracing_span.c */
+extern void begin_span(TraceId trace_id, Span * span,
+					   SpanType type, uint64 *span_id, uint64 parent_id, uint64 query_id, const TimestampTz *start_span);
+extern void end_span(Span * span, const TimestampTz *end_time);
+extern SpanType command_type_to_span_type(CmdType cmd_type);
+extern const char *get_span_type(const Span * span, const char *qbuffer, Size qbuffer_size);
+extern const char *get_operation_name(const Span * span, const char *qbuffer, Size qbuffer_size);
+extern void adjust_file_offset(Span * span, Size file_position);
+extern bool traceid_zero(TraceId trace_id);
+
+#endif
diff --git a/contrib/pg_tracing/pg_tracing_query_process.c b/contrib/pg_tracing/pg_tracing_query_process.c
new file mode 100644
index 0000000000..006d496e9a
--- /dev/null
+++ b/contrib/pg_tracing/pg_tracing_query_process.c
@@ -0,0 +1,534 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_tracing_query_process.c
+ * 		pg_tracing query processing functions.
+ *
+ * IDENTIFICATION
+ *	  contrib/pg_tracing/pg_tracing_explain.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <sys/stat.h>
+#include <unistd.h>
+#include "parser/scanner.h"
+#include "pg_tracing.h"
+#include "nodes/extensible.h"
+
+/*
+ * Check if we have a comment that could store SQLComment information at the
+ * start or end of the query.
+ * Returns 0 if there's possible comment at the begining, 1 at the end and -1
+ * if no comments were detected.
+ */
+static int
+has_possible_sql_comment(const char *query)
+{
+	int			query_len = strlen(query);
+	int			end = query_len - 1;
+
+	/*
+	 * We need at least a comment with 'traceparent' information.
+	 * "traceparent" + "=" + '' -> 13 characters.
+	 * 00-00000000000000000000000000000009-0000000000000005-01 -> 55
+	 * characters. Comments start and ends -> 4 characters.
+	 */
+	if (query_len < 55 + 13 + 4)
+		return -1;
+
+	/* Check if we have comment at the begining */
+	if (query[0] == '/' && query[1] == '*')
+		return 0;
+
+	/* Ignore the ; at the end if present */
+	if (query[end] == ';')
+		end--;
+
+	/* Check end of comment at the end of the query */
+	if (query[end - 1] == '*' && query[end] == '/')
+		return 1;
+
+	return -1;
+}
+
+/*
+ * Parse trace parent values.
+ * The expected format for traceparent is: version-traceid-parentid-sampled
+ * Example: 00-00000000000000000000000000000009-0000000000000005-01
+ */
+static pgTracingTraceparent
+parse_traceparent_value(const char *traceparent_str)
+{
+	pgTracingTraceparent traceparent;
+	char	   *traceid_left;
+	char	   *endptr;
+
+	traceparent.trace_id.traceid_left = 0;
+	traceparent.trace_id.traceid_right = 0;
+	traceparent.sampled = 0;
+
+	/* Check that '-' are at the expected places */
+	if (traceparent_str[2] != '-' ||
+		traceparent_str[35] != '-' ||
+		traceparent_str[52] != '-')
+		return traceparent;
+
+	/* Parse traceparent parameters */
+	errno = 0;
+
+	traceid_left = pnstrdup(&traceparent_str[3], 16);
+	traceparent.trace_id.traceid_left = strtou64(traceid_left, &endptr, 16);
+	pfree(traceid_left);
+	traceparent.trace_id.traceid_right = strtou64(&traceparent_str[3 + 16], &endptr, 16);
+
+	if (endptr != traceparent_str + 35 || errno)
+		return traceparent;
+	traceparent.parent_id = strtou64(&traceparent_str[36], &endptr, 16);
+	if (endptr != traceparent_str + 52 || errno)
+		return traceparent;
+	traceparent.sampled = strtol(&traceparent_str[53], &endptr, 16);
+	if (endptr != traceparent_str + 55 || errno)
+		/* Just to be sure, reset sampled on error */
+		traceparent.sampled = 0;
+
+	return traceparent;
+}
+
+/*
+ * Extract trace context from a SQLCommenter value in a query
+ *
+ * We're expecting the query to start or end with a SQLComment containing the
+ * traceparent parameter
+ * "/\*traceparent='00-00000000000000000000000000000009-0000000000000005-01'*\/ SELECT 1;"
+ * Or "SELECT 1 /\*traceparent='00-00000000000000000000000000000009-0000000000000005-01'*\/;"
+ */
+void
+extract_trace_context_from_query(pgTracingTraceContext * trace_context,
+								 const char *query)
+{
+	const char *start_sqlcomment = NULL;
+	const char *end_sqlcomment;
+
+	int			possible_comment = has_possible_sql_comment(query);
+
+	if (possible_comment == 0)
+	{
+		/* We have a possible comment at the begining */
+		start_sqlcomment = query;
+		end_sqlcomment = strstr(query, "*/");
+		if (end_sqlcomment == NULL)
+			return;
+	}
+	else if (possible_comment == 1)
+	{
+		/* We have a possible comment at the end */
+		start_sqlcomment = strstr(query, "/*");
+		if (start_sqlcomment == NULL)
+			return;
+		end_sqlcomment = strstr(start_sqlcomment, "*/");
+		if (end_sqlcomment == NULL)
+			return;
+	}
+	else
+	{
+		/* No sqlcomment, bail out */
+		return;
+	}
+
+	parse_trace_context(trace_context, start_sqlcomment,
+						end_sqlcomment - start_sqlcomment);
+}
+
+/*
+ * Parse trace context value.
+ * We're only interested in traceparent value to get the trace id, parent id and sampled flag.
+ */
+void
+parse_trace_context(pgTracingTraceContext * trace_context, const char *trace_context_str,
+					int trace_context_len)
+{
+	const char *traceparent_str;
+	const char *end_trace_context;
+
+	end_trace_context = trace_context_str + trace_context_len;
+
+	/*
+	 * Locate traceparent parameter and make sure it has the expected size
+	 * "traceparent" + "=" + '' -> 13 characters
+	 * 00-00000000000000000000000000000009-0000000000000005-01 -> 55
+	 * characters
+	 */
+	traceparent_str = strstr(trace_context_str, "traceparent='");
+	if (traceparent_str == NULL ||
+		traceparent_str > end_trace_context ||
+		end_trace_context - traceparent_str < 55 + 13)
+		return;
+
+	/* Move to the start of the traceparent values */
+	traceparent_str = traceparent_str + 13;
+	/* And parse it */
+	trace_context->traceparent = parse_traceparent_value(traceparent_str);
+}
+
+/*
+ * comp_location: comparator for qsorting LocationLen structs by location
+ */
+static int
+comp_location(const void *a, const void *b)
+{
+	int			l = ((const LocationLen *) a)->location;
+	int			r = ((const LocationLen *) b)->location;
+
+	if (l < r)
+		return -1;
+	else if (l > r)
+		return +1;
+	else
+		return 0;
+}
+
+
+/*
+ * Normalise query: Comments are removed, Constants are replaced by $x, all tokens
+ * are separated by a single space.
+ * Parameters are put in the param_str wich will contain all parameters values
+ * using the format: "$1 = 0, $2 = 'v'"
+ */
+const char *
+normalise_query_parameters(const JumbleState *jstate, const char *query,
+						   int query_loc, int *query_len_p, char **param_str,
+						   int *param_len)
+{
+	char	   *norm_query;
+	int			query_len = *query_len_p;
+	int			norm_query_buflen,	/* Space allowed for norm_query */
+				n_quer_loc = 0;
+	LocationLen *locs;
+	core_yyscan_t yyscanner;
+	core_yy_extra_type yyextra;
+	core_YYSTYPE yylval;
+	YYLTYPE		yylloc;
+	int			current_loc = 0;
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+
+	if (query_loc == -1)
+	{
+		/* If query location is unknown, distrust query_len as well */
+		query_loc = 0;
+		query_len = strlen(query);
+	}
+	else
+	{
+		/* Length of 0 (or -1) means "rest of string" */
+		if (query_len <= 0)
+			query_len = strlen(query);
+		else
+			Assert(query_len <= strlen(query));
+	}
+
+	norm_query_buflen = query_len + jstate->clocations_count * 10;
+	Assert(norm_query_buflen > 0);
+	locs = jstate->clocations;
+
+	/* Allocate result buffer */
+	norm_query = palloc(norm_query_buflen + 1);
+
+	if (jstate->clocations_count > 1)
+		qsort(jstate->clocations, jstate->clocations_count,
+			  sizeof(LocationLen), comp_location);
+
+	/* initialize the flex scanner --- should match raw_parser() */
+	yyscanner = scanner_init(query + query_loc,
+							 &yyextra,
+							 &ScanKeywords,
+							 ScanKeywordTokens);
+
+	for (;;)
+	{
+		int			loc = locs[current_loc].location;
+		int			tok;
+
+		loc -= query_loc;
+
+		tok = core_yylex(&yylval, &yylloc, yyscanner);
+
+		/*
+		 * We should not hit end-of-string, but if we do, behave sanely
+		 */
+		if (tok == 0)
+			break;				/* out of inner for-loop */
+		if (yylloc > query_len)
+			break;
+
+		/*
+		 * We should find the token position exactly, but if we somehow run
+		 * past it, work with that.
+		 */
+		if (current_loc < jstate->clocations_count && yylloc >= loc)
+		{
+			appendStringInfo(&buf,
+							 "%s$%d = ",
+							 current_loc > 0 ? ", " : "",
+							 current_loc + 1);
+			if (query[loc] == '-')
+			{
+				/*
+				 * It's a negative value - this is the one and only case where
+				 * we replace more than a single token.
+				 *
+				 * Do not compensate for the core system's special-case
+				 * adjustment of location to that of the leading '-' operator
+				 * in the event of a negative constant.  It is also useful for
+				 * our purposes to start from the minus symbol.  In this way,
+				 * queries like "select * from foo where bar = 1" and "select *
+				 * from foo where bar = -2" will have identical normalized
+				 * query strings.
+				 */
+				appendStringInfoChar(&buf, '-');
+				tok = core_yylex(&yylval, &yylloc, yyscanner);
+				if (tok == 0)
+					break;		/* out of inner for-loop */
+			}
+			if (yylloc > 0 && yyextra.scanbuf[yylloc - 1] == ' ' && n_quer_loc > 0)
+			{
+				norm_query[n_quer_loc++] = ' ';
+			}
+
+			/*
+			 * Append the current parameter $x in the normalised query
+			 */
+			n_quer_loc += sprintf(norm_query + n_quer_loc, "$%d",
+								  current_loc + 1 + jstate->highest_extern_param_id);
+
+			appendStringInfoString(&buf, yyextra.scanbuf + yylloc);
+
+			current_loc++;
+		}
+		else
+		{
+			int			to_copy;
+
+			if (yylloc > 0 && yyextra.scanbuf[yylloc - 1] == ' ' && n_quer_loc > 0)
+			{
+				norm_query[n_quer_loc++] = ' ';
+			}
+			to_copy = strlen(yyextra.scanbuf + yylloc);
+			Assert(n_quer_loc + to_copy < norm_query_buflen + 1);
+			memcpy(norm_query + n_quer_loc, yyextra.scanbuf + yylloc, to_copy);
+			n_quer_loc += to_copy;
+		}
+	}
+	scanner_finish(yyscanner);
+
+	*query_len_p = n_quer_loc;
+	norm_query[n_quer_loc] = '\0';
+	*param_str = buf.data;
+	*param_len = buf.len;
+	return norm_query;
+}
+
+/*
+ * Normalise query: tokens will be separated by a single space
+ */
+const char *
+normalise_query(const char *query, int query_loc, int *query_len_p)
+{
+	char	   *norm_query;
+	int			query_len = *query_len_p;
+	int			norm_query_buflen = query_len;
+	int			n_quer_loc = 0;
+	core_yyscan_t yyscanner;
+	core_yy_extra_type yyextra;
+	core_YYSTYPE yylval;
+	YYLTYPE		yylloc;
+
+	/* Allocate result buffer */
+	norm_query = palloc(norm_query_buflen + 2);
+
+	/* initialize the flex scanner --- should match raw_parser() */
+	yyscanner = scanner_init(query + query_loc, &yyextra, &ScanKeywords,
+							 ScanKeywordTokens);
+	for (;;)
+	{
+		int			tok;
+		int			to_copy;
+
+		tok = core_yylex(&yylval, &yylloc, yyscanner);
+
+		if (tok == 0)
+			break;				/* out of inner for-loop */
+		if (yylloc > query_len)
+			break;
+
+		if (yylloc > 0 && yyextra.scanbuf[yylloc - 1] == ' ' && n_quer_loc > 0)
+		{
+			norm_query[n_quer_loc++] = ' ';
+		}
+		to_copy = strlen(yyextra.scanbuf + yylloc);
+		Assert(n_quer_loc + to_copy < norm_query_buflen + 2);
+		memcpy(norm_query + n_quer_loc, yyextra.scanbuf + yylloc, to_copy);
+		n_quer_loc += to_copy;
+	}
+	scanner_finish(yyscanner);
+
+	*query_len_p = n_quer_loc;
+	norm_query[n_quer_loc] = '\0';
+	return norm_query;
+}
+
+/*
+ * Store text in the pg_tracing stat file
+ */
+bool
+text_store_file(pgTracingSharedState * pg_tracing, const char *text, int text_len,
+				Size *query_offset)
+{
+	Size		off;
+	int			fd;
+
+	off = pg_tracing->extent;
+	pg_tracing->extent += text_len + 1;
+
+	/*
+	 * Don't allow the file to grow larger than what qtext_load_file can
+	 * (theoretically) handle.  This has been seen to be reachable on 32-bit
+	 * platforms.
+	 */
+	if (unlikely(text_len >= MaxAllocHugeSize - off))
+	{
+		errno = EFBIG;			/* not quite right, but it'll do */
+		fd = -1;
+		goto error;
+	}
+
+	/* Now write the data into the successfully-reserved part of the file */
+	fd = OpenTransientFile(PG_TRACING_TEXT_FILE, O_RDWR | O_CREAT | PG_BINARY);
+	if (fd < 0)
+		goto error;
+
+	if (pg_pwrite(fd, text, text_len, off) != text_len)
+		goto error;
+	if (pg_pwrite(fd, "\0", 1, off + text_len) != 1)
+		goto error;
+
+	CloseTransientFile(fd);
+
+	/*
+	 * Set offset once write was succesful
+	 */
+	*query_offset = off;
+
+	return true;
+
+error:
+	ereport(LOG,
+			(errcode_for_file_access(),
+			 errmsg("could not write file \"%s\": %m",
+					PG_TRACING_TEXT_FILE)));
+
+	if (fd >= 0)
+		CloseTransientFile(fd);
+
+	return false;
+}
+
+/*
+ * Read the external query text file into a malloc'd buffer.
+ *
+ * Returns NULL (without throwing an error) if unable to read, eg file not
+ * there or insufficient memory.
+ *
+ * On success, the buffer size is also returned into *buffer_size.
+ *
+ * This can be called without any lock on pgss->lock, but in that case the
+ * caller is responsible for verifying that the result is sane.
+ */
+const char *
+qtext_load_file(Size *buffer_size)
+{
+	char	   *buf;
+	int			fd;
+	struct stat stat;
+	Size		nread;
+
+	fd = OpenTransientFile(PG_TRACING_TEXT_FILE, O_RDONLY | PG_BINARY);
+	if (fd < 0)
+	{
+		if (errno != ENOENT)
+			ereport(LOG,
+					(errcode_for_file_access(),
+					 errmsg("could not read file \"%s\": %m",
+							PG_TRACING_TEXT_FILE)));
+		return NULL;
+	}
+
+	/* Get file length */
+	if (fstat(fd, &stat))
+	{
+		ereport(LOG,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						PG_TRACING_TEXT_FILE)));
+		CloseTransientFile(fd);
+		return NULL;
+	}
+
+	/* Allocate buffer; beware that off_t might be wider than size_t */
+	if (stat.st_size <= MaxAllocHugeSize)
+		buf = (char *) malloc(stat.st_size);
+	else
+		buf = NULL;
+	if (buf == NULL)
+	{
+		ereport(LOG,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("out of memory"),
+				 errdetail("Could not allocate enough memory to read file \"%s\".",
+						   PG_TRACING_TEXT_FILE)));
+		CloseTransientFile(fd);
+		return NULL;
+	}
+
+	/*
+	 * OK, slurp in the file.  Windows fails if we try to read more than
+	 * INT_MAX bytes at once, and other platforms might not like that either,
+	 * so read a very large file in 1GB segments.
+	 */
+	nread = 0;
+	while (nread < stat.st_size)
+	{
+		int			toread = Min(1024 * 1024 * 1024, stat.st_size - nread);
+
+		/*
+		 * If we get a short read and errno doesn't get set, the reason is
+		 * probably that garbage collection truncated the file since we did
+		 * the fstat(), so we don't log a complaint --- but we don't return
+		 * the data, either, since it's most likely corrupt due to concurrent
+		 * writes from garbage collection.
+		 */
+		errno = 0;
+		if (read(fd, buf + nread, toread) != toread)
+		{
+			if (errno)
+				ereport(LOG,
+						(errcode_for_file_access(),
+						 errmsg("could not read file \"%s\": %m",
+								PG_TRACING_TEXT_FILE)));
+			free(buf);
+			CloseTransientFile(fd);
+			return NULL;
+		}
+		nread += toread;
+	}
+
+	if (CloseTransientFile(fd) != 0)
+		ereport(LOG,
+				(errcode_for_file_access(),
+				 errmsg("could not close file \"%s\": %m", PG_TRACING_TEXT_FILE)));
+
+	*buffer_size = nread;
+	return buf;
+}
diff --git a/contrib/pg_tracing/pg_tracing_span.c b/contrib/pg_tracing/pg_tracing_span.c
new file mode 100644
index 0000000000..f97adc46c2
--- /dev/null
+++ b/contrib/pg_tracing/pg_tracing_span.c
@@ -0,0 +1,261 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_tracing_span.c
+ * 		pg_tracing span functions.
+ *
+ * IDENTIFICATION
+ *	  contrib/pg_tracing/pg_tracing_span.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "nodes/extensible.h"
+#include "pg_tracing.h"
+#include "common/pg_prng.h"
+
+/*
+ * Convert a node CmdType to the matching SpanType
+ */
+SpanType
+command_type_to_span_type(CmdType cmd_type)
+{
+	switch (cmd_type)
+	{
+		case CMD_SELECT:
+			return SPAN_TOP_SELECT;
+		case CMD_INSERT:
+			return SPAN_TOP_INSERT;
+		case CMD_UPDATE:
+			return SPAN_TOP_UPDATE;
+		case CMD_DELETE:
+			return SPAN_TOP_DELETE;
+		case CMD_MERGE:
+			return SPAN_TOP_MERGE;
+		case CMD_UTILITY:
+			return SPAN_TOP_UTILITY;
+		case CMD_NOTHING:
+			return SPAN_TOP_NOTHING;
+		case CMD_UNKNOWN:
+			return SPAN_TOP_UNKNOWN;
+	}
+	return SPAN_TOP_UNKNOWN;
+}
+
+/*
+ * Initialize span fields
+ */
+static void
+initialize_span_fields(Span * span, SpanType type, TraceId trace_id, uint64 *span_id, uint64 parent_id, uint64 query_id)
+{
+	span->trace_id = trace_id;
+	span->type = type;
+
+	/*
+	 * If parent id is unset, it means that there's no propagated trace
+	 * informations from the caller. In this case, this is the top span,
+	 * span_id == parent_id and we reuse the generated trace id for the span
+	 * id.
+	 */
+	if (parent_id == 0)
+		span->parent_id = trace_id.traceid_right;
+	else
+		span->parent_id = parent_id;
+	if (span_id != NULL)
+		span->span_id = *span_id;
+	else
+		span->span_id = pg_prng_uint64(&pg_global_prng_state);
+
+	span->node_type_offset = -1;
+	span->operation_name_offset = -1;
+	span->parameter_offset = -1;
+	span->sql_error_code = 0;
+	span->startup = 0;
+	span->be_pid = MyProcPid;
+	span->database_id = MyDatabaseId;
+	span->user_id = GetUserId();
+	span->subxact_count = MyProc->subxidStatus.count;
+	span->ended = false;
+	span->query_id = query_id;
+	memset(&span->node_counters, 0, sizeof(NodeCounters));
+	memset(&span->plan_counters, 0, sizeof(PlanCounters));
+
+	/*
+	 * Store the starting buffer for planner and process utility spans
+	 */
+	if (type == SPAN_PLANNER || span->type == SPAN_PROCESS_UTILITY)
+	{
+		/* TODO Do we need to track wal for planner? */
+		span->node_counters.buffer_usage = pgBufferUsage;
+		span->node_counters.wal_usage = pgWalUsage;
+	}
+}
+
+/*
+ * Initialize span and set span starting time.
+ */
+void
+begin_span(TraceId trace_id, Span * span, SpanType type,
+		   uint64 *span_id, uint64 parent_id, uint64 query_id,
+		   const TimestampTz *input_start_span_time)
+{
+	TimestampTz start_span_time;
+
+	initialize_span_fields(span, type, trace_id, span_id, parent_id, query_id);
+
+	/* If no start span is provided, get the current one */
+	if (input_start_span_time == NULL)
+		start_span_time = GetCurrentTimestamp();
+	else
+		start_span_time = *input_start_span_time;
+
+	span->start = start_span_time;
+}
+
+/*
+ * Set span duration and accumulated buffers.
+ * end_span_input is optional, if NULL is passed, we use
+ * the current time
+ */
+void
+end_span(Span * span, const TimestampTz *end_time_input)
+{
+	BufferUsage buffer_usage;
+	WalUsage	wal_usage;
+
+	Assert(!traceid_zero(span->trace_id));
+
+	/* Span should be ended only once */
+	Assert(!span->ended);
+	span->ended = true;
+
+	/* Set span duration with the end time before substrating the start */
+	if (end_time_input == NULL)
+		span->end = GetCurrentTimestamp();
+	else
+		span->end = *end_time_input;
+
+
+	if (span->type == SPAN_PLANNER || span->type == SPAN_PROCESS_UTILITY)
+	{
+		/* calc differences of buffer counters. */
+		memset(&buffer_usage, 0, sizeof(BufferUsage));
+		BufferUsageAccumDiff(&buffer_usage, &pgBufferUsage,
+							 &span->node_counters.buffer_usage);
+		span->node_counters.buffer_usage = buffer_usage;
+
+		/* calc differences of WAL counters. */
+		memset(&wal_usage, 0, sizeof(wal_usage));
+		WalUsageAccumDiff(&wal_usage, &pgWalUsage,
+						  &span->node_counters.wal_usage);
+		span->node_counters.wal_usage = wal_usage;
+	}
+}
+
+/*
+ * Get the type of a span.
+ * If it is a node span, the name may be pulled from the stat file.
+ */
+const char *
+get_span_type(const Span * span, const char *qbuffer, Size qbuffer_size)
+{
+	if (span->node_type_offset != -1 && qbuffer_size > 0 &&
+		span->node_type_offset <= qbuffer_size)
+	{
+		StringInfo	node_name = makeStringInfo();
+
+		appendStringInfoString(node_name, qbuffer + span->node_type_offset);
+		return node_name->data;
+	}
+
+	switch (span->type)
+	{
+		case SPAN_PLANNER:
+			return "Planner";
+		case SPAN_FUNCTION:
+			return "Function";
+		case SPAN_PROCESS_UTILITY:
+			return "ProcessUtility";
+		case SPAN_EXECUTOR_RUN:
+			return "Executor";
+		case SPAN_EXECUTOR_FINISH:
+			return "Executor";
+
+		case SPAN_TOP_SELECT:
+			return "Select query";
+		case SPAN_TOP_INSERT:
+			return "Insert query";
+		case SPAN_TOP_UPDATE:
+			return "Update query";
+		case SPAN_TOP_DELETE:
+			return "Delete query";
+		case SPAN_TOP_MERGE:
+			return "Merge query";
+		case SPAN_TOP_UTILITY:
+			return "Utility query";
+		case SPAN_TOP_NOTHING:
+			return "Nothing query";
+		case SPAN_TOP_UNKNOWN:
+			return "Unknown query";
+	}
+	return "???";
+}
+
+/*
+ * Get the operation of a span.
+ * For node span, the name may be pulled from the stat file.
+ */
+const char *
+get_operation_name(const Span * span, const char *qbuffer, Size qbuffer_size)
+{
+	if (span->operation_name_offset != -1 && qbuffer_size > 0
+		&& span->operation_name_offset <= qbuffer_size)
+		return qbuffer + span->operation_name_offset;
+
+	switch (span->type)
+	{
+		case SPAN_PLANNER:
+			return "Planner";
+		case SPAN_FUNCTION:
+			return "Function";
+		case SPAN_PROCESS_UTILITY:
+			return "ProcessUtility";
+		case SPAN_EXECUTOR_RUN:
+			return "ExecutorRun";
+		case SPAN_EXECUTOR_FINISH:
+			return "ExecutorFinish";
+		case SPAN_TOP_SELECT:
+		case SPAN_TOP_INSERT:
+		case SPAN_TOP_UPDATE:
+		case SPAN_TOP_DELETE:
+		case SPAN_TOP_MERGE:
+		case SPAN_TOP_UTILITY:
+		case SPAN_TOP_NOTHING:
+		case SPAN_TOP_UNKNOWN:
+			return "Top";
+	}
+	return "Unknown type";
+}
+
+/*
+ * Adjust span's offsets with the provided file offset
+ */
+void
+adjust_file_offset(Span * span, Size file_position)
+{
+	if (span->node_type_offset != -1)
+		span->node_type_offset += file_position;
+	if (span->operation_name_offset != -1)
+		span->operation_name_offset += file_position;
+	if (span->parameter_offset != -1)
+		span->parameter_offset += file_position;
+}
+
+/*
+* Returns true if TraceId is zero
+*/
+bool
+traceid_zero(TraceId trace_id)
+{
+	return trace_id.traceid_left == 0 && trace_id.traceid_right == 0;
+}
diff --git a/contrib/pg_tracing/sql/cleanup.sql b/contrib/pg_tracing/sql/cleanup.sql
new file mode 100644
index 0000000000..43b54835de
--- /dev/null
+++ b/contrib/pg_tracing/sql/cleanup.sql
@@ -0,0 +1,5 @@
+-- Drop test table and test functions
+DROP TABLE pg_tracing_test;
+DROP function test_function_project_set;
+DROP function test_function_result;
+DROP EXTENSION pg_tracing CASCADE;
diff --git a/contrib/pg_tracing/sql/extended.sql b/contrib/pg_tracing/sql/extended.sql
new file mode 100644
index 0000000000..7f42f41c90
--- /dev/null
+++ b/contrib/pg_tracing/sql/extended.sql
@@ -0,0 +1,41 @@
+-- Trace everything
+SET pg_tracing.sample_rate = 1.0;
+
+-- Simple query with extended protocol
+SELECT $1, $2 \bind 1 2 \g
+SELECT span_type, span_operation, parameters, lvl FROM peek_ordered_spans;
+CALL clean_spans();
+
+-- Trigger an error due to mismatching number of parameters
+BEGIN; select $1 \bind \g
+ROLLBACK;
+SELECT span_type, span_operation, parameters, lvl FROM peek_ordered_spans;
+CALL clean_spans();
+
+-- Execute queries with extended protocol within an explicit transaction
+BEGIN;
+SELECT $1 \bind 1 \g
+SELECT $1, $2 \bind 2 3 \g
+COMMIT;
+
+-- Spans within the same transaction should have been generated with the same trace_id
+SELECT count(distinct(trace_id)) = 1 FROM pg_tracing_peek_spans;
+SELECT span_type, span_operation, parameters, lvl FROM peek_ordered_spans;
+CALL clean_spans();
+
+-- Mix extended protocol and simple protocol
+BEGIN;
+SELECT $1 \bind 1 \g
+SELECT 5, 6, 7;
+SELECT $1, $2 \bind 2 3 \g
+COMMIT;
+
+-- Spans within the same transaction should have been generated with the same trace_id
+SELECT count(distinct(trace_id)) = 1 FROM pg_tracing_peek_spans;
+SELECT span_type, span_operation, parameters, lvl FROM peek_ordered_spans;
+CALL clean_spans();
+
+-- gdesc calls a single parse command then execute a query. Make sure we handle this case
+SELECT 1 \gdesc
+SELECT span_type, span_operation, parameters, lvl FROM peek_ordered_spans;
+CALL clean_spans();
diff --git a/contrib/pg_tracing/sql/full_buffer.sql b/contrib/pg_tracing/sql/full_buffer.sql
new file mode 100644
index 0000000000..d046b323aa
--- /dev/null
+++ b/contrib/pg_tracing/sql/full_buffer.sql
@@ -0,0 +1,30 @@
+-- A simple procedure creating nested calls
+CREATE OR REPLACE PROCEDURE loop_select(iterations int) AS
+$BODY$
+BEGIN
+    FOR i IN 1..iterations LOOP
+        PERFORM 'SELECT 1;';
+    END LOOP;
+END;
+$BODY$
+LANGUAGE plpgsql;
+
+-- Clear stats
+select * from pg_tracing_reset();
+-- Check initial stats after reset
+select processed_traces, processed_spans, dropped_traces, dropped_spans from pg_tracing_info();
+
+-- Saturate the span buffer. Each call should create at least 2 spans
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ CALL loop_select(20);
+-- Check that we have dropped spans. The trace was still partially processed
+select processed_traces = 1, processed_spans = 40, dropped_traces = 0, dropped_spans > 0 from pg_tracing_info();
+
+-- Try to create new traces while buffer is full
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ SELECT 1;
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-00'*/ SELECT 1;
+
+-- We should have only one additional dropped trace
+select processed_traces = 1, processed_spans = 40, dropped_traces = 1 from pg_tracing_info();
+
+-- Clean current spans
+CALL clean_spans();
diff --git a/contrib/pg_tracing/sql/insert.sql b/contrib/pg_tracing/sql/insert.sql
new file mode 100644
index 0000000000..f883688f76
--- /dev/null
+++ b/contrib/pg_tracing/sql/insert.sql
@@ -0,0 +1,22 @@
+-- Only trace queries with sample flag
+SET pg_tracing.sample_rate = 0.0;
+SET pg_tracing.caller_sample_rate = 1.0;
+
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ CREATE TABLE IF NOT EXISTS pg_tracing_test_table_with_constraint (a int, b char(20), CONSTRAINT PK_tracing_test PRIMARY KEY (a));
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000001';
+
+-- Simple insertion
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ INSERT INTO pg_tracing_test_table_with_constraint VALUES(1, 'aaa');
+SELECT span_type, span_operation from peek_ordered_spans where trace_id='00000000000000000000000000000002';
+
+-- Trigger constraint violation
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-01'*/ INSERT INTO pg_tracing_test_table_with_constraint VALUES(1, 'aaa');
+SELECT span_type, span_operation, sql_error_code, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000003';
+
+-- Trigger an error while calling pg_tracing_peek_spans which resets tracing, nothing should be generated
+CALL clean_spans();
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000004-0000000000000004-01'*/ INSERT INTO pg_tracing_test_table_with_constraint VALUES(length((select sql_error_code from public.pg_tracing_peek_spans)), 'aaa');
+SELECT span_type, span_operation, sql_error_code from peek_ordered_spans where trace_id='00000000000000000000000000000004';
+
+-- Cleanup
+CALL clean_spans();
diff --git a/contrib/pg_tracing/sql/nested.sql b/contrib/pg_tracing/sql/nested.sql
new file mode 100644
index 0000000000..1f0af18fe0
--- /dev/null
+++ b/contrib/pg_tracing/sql/nested.sql
@@ -0,0 +1,129 @@
+-- Create test function to sample
+CREATE OR REPLACE FUNCTION test_function_project_set(a int) RETURNS SETOF oid AS
+$BODY$
+BEGIN
+	RETURN QUERY SELECT oid from pg_class where oid = a;
+END;
+$BODY$
+LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION test_function_result(a int, b text) RETURNS void AS
+$BODY$
+BEGIN
+    INSERT INTO pg_tracing_test(a, b) VALUES (a, b);
+END;
+$BODY$
+LANGUAGE plpgsql;
+
+
+-- Trace a statement with a function call
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000051-0000000000000051-01'*/ select test_function_project_set(1);
+
+-- The test function call will generate the following spans (non exhaustive list):
+-- +------------------------------------------------------------+
+-- | A: Select test_function_project_set(1);                    |
+-- +-+----------+-+------------------------------------------+--+
+--   |C: Planner| |E: ExecutorRun                            |
+--   +----------+ +-----------+------------------------------+
+--                            | I: Select a from b where...  |
+--                            +---+--------------+-----------+
+--                                |J: ExecutorRun|
+--                                +--------------+
+
+-- Gather span_id, span start and span end of function call statement
+SELECT span_id AS span_a_id,
+        get_span_start(span_start) as span_a_start,
+        get_span_end(span_start) as span_a_end
+		from pg_tracing_peek_spans where parent_id='0000000000000051' \gset
+SELECT span_id AS span_e_id,
+        get_span_start(span_start) as span_e_start,
+        get_span_end(span_start) as span_e_end
+		from pg_tracing_peek_spans where parent_id=:'span_a_id' and span_type='Executor' and span_operation='ExecutorRun' \gset
+SELECT span_id AS span_i_id,
+        get_span_start(span_start) as span_i_start,
+        get_span_end(span_start) as span_i_end
+		from pg_tracing_peek_spans where parent_id=:'span_e_id' and span_type='Select query' \gset
+SELECT span_id AS span_j_id,
+        get_span_start(span_start) as span_j_start,
+        get_span_end(span_start) as span_j_end
+		from pg_tracing_peek_spans where parent_id=:'span_i_id' and span_operation='ExecutorRun' \gset
+
+-- Check that spans' start and end are within expection
+SELECT :span_a_start <= :span_e_start AS top_query_before_run,
+		:span_a_end >= :span_e_end AS top_ends_after_run_end;
+
+-- Check that the root span is the longest one
+WITH max_end AS (select max(span_end) from pg_tracing_peek_spans)
+SELECT span_end = max_end.max from pg_tracing_peek_spans, max_end
+    where span_id = :'span_a_id';
+
+-- Check tracking with top tracking
+SET pg_tracing.track = 'top';
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000052-0000000000000052-01'*/ select test_function_project_set(1);
+SELECT count(*) from pg_tracing_consume_spans where trace_id='00000000000000000000000000000052';
+
+-- Check tracking with no tracking
+SET pg_tracing.track = 'none';
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000053-0000000000000053-01'*/ select test_function_project_set(1);
+SELECT count(*) from pg_tracing_consume_spans where trace_id='00000000000000000000000000000053';
+
+-- Reset tracking setting
+SET pg_tracing.track TO DEFAULT;
+
+-- Create test procedure
+CREATE OR REPLACE PROCEDURE sum_one(i int) AS $$
+DECLARE
+  r int;
+BEGIN
+  SELECT (i + i)::int INTO r;
+END; $$ LANGUAGE plpgsql;
+
+-- Test tracking of procedure with utility tracking enabled
+SET pg_tracing.track_utility=on;
+/*traceparent='00-00000000000000000000000000000054-0000000000000054-01'*/ CALL sum_one(3);
+select span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000054';
+
+-- Test again with utility tracking disabled
+SET pg_tracing.track_utility=off;
+/*traceparent='00-00000000000000000000000000000055-0000000000000055-01'*/ CALL sum_one(10);
+select span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000055';
+
+-- Create immutable function
+CREATE OR REPLACE FUNCTION test_immutable_function(a int) RETURNS oid
+AS 'SELECT oid from pg_class where oid = a;'
+LANGUAGE sql IMMUTABLE;
+
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000056-0000000000000056-01'*/ select test_immutable_function(1);
+select span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000056';
+
+-- +-------------------------------------------------------------------+
+-- | A: Select test_function(1);                                       |
+-- +-+----------++----------------++-------------------------------+---+
+--   |C: Planner||D: ExecutorStart||E: ExecutorRun                 |
+--   +----------++----------------+++------------------------------+
+--                                   |H: Insert INTO...    |
+--                                   +--+--------------+---+
+--                                      |I: ExecutorRun|
+--                                      +--------------+
+
+-- Check function with result node
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000058-0000000000000058-01'*/ select test_function_result(1, 'test');
+
+-- Gather span_id, span start and span end of function call statement
+SELECT span_id AS span_a_id,
+        get_span_start(span_start) as span_a_start,
+        get_span_end(span_start) as span_a_end
+		from pg_tracing_peek_spans where parent_id='0000000000000058' \gset
+SELECT span_id AS span_e_id,
+        get_span_start(span_start) as span_e_start,
+        get_span_end(span_start) as span_e_end
+		from pg_tracing_peek_spans where parent_id=:'span_a_id' and span_type='Executor' and span_operation='ExecutorRun' \gset
+SELECT span_id AS span_h_id,
+        get_span_start(span_start) as span_h_start,
+        get_span_end(span_start) as span_h_end
+		from pg_tracing_peek_spans where parent_id=:'span_e_id' and span_type='Insert query' \gset
+
+select span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000058';
+
+-- Cleanup
+CALL clean_spans();
diff --git a/contrib/pg_tracing/sql/reset.sql b/contrib/pg_tracing/sql/reset.sql
new file mode 100644
index 0000000000..531db2172d
--- /dev/null
+++ b/contrib/pg_tracing/sql/reset.sql
@@ -0,0 +1,3 @@
+-- Check reset is working
+SELECT pg_tracing_reset();
+SELECT traces from pg_tracing_info;
diff --git a/contrib/pg_tracing/sql/sample.sql b/contrib/pg_tracing/sql/sample.sql
new file mode 100644
index 0000000000..a732c9eb52
--- /dev/null
+++ b/contrib/pg_tracing/sql/sample.sql
@@ -0,0 +1,49 @@
+-- Trace nothing
+SET pg_tracing.sample_rate = 0.0;
+SET pg_tracing.caller_sample_rate = 0.0;
+
+-- Query with sampling flag
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ SELECT 1;
+select count(distinct(trace_id))=0 from pg_tracing_consume_spans;
+
+-- Query without trace context
+SELECT 1;
+select count(distinct(trace_id))=0 from pg_tracing_consume_spans;
+
+-- Enable full sampling
+SET pg_tracing.sample_rate = 1.0;
+
+-- Generate queries with sampling flag on, off and no trace context
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ SELECT 1;
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-00'*/ SELECT 2;
+SELECT 3;
+SELECT 4;
+
+-- Check number of generated spans
+select count(distinct(trace_id)) from pg_tracing_peek_spans;
+-- Check span order for full sampling
+select span_operation, parameters, lvl from peek_ordered_spans;
+-- Top spans should reuse generated ids and have trace_id = parent_id
+select span_operation, parameters from peek_ordered_spans where right(trace_id, 16) = parent_id;
+CALL clean_spans();
+
+-- Only trace query with sampled flag
+SET pg_tracing.sample_rate = 0.0;
+SET pg_tracing.caller_sample_rate = 1.0;
+
+-- Clean set call spans
+CALL clean_spans();
+
+-- Generate queries with sampling flag on, off, no trace context and SQLComment at the end
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000004-0000000000000004-01'*/ SELECT 1;
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000005-0000000000000005-00'*/ SELECT 2;
+SELECT 3 /*dddbs='postgres.db',traceparent='00-00000000000000000000000000000006-0000000000000006-01'*/;
+/*dddbs='postgres.db',traceparent='00-fffffffffffffffff000000000000007-0000000000000007-01'*/ SELECT 4;
+SELECT 1;
+
+-- Check number of generated spans
+select distinct(trace_id) from pg_tracing_peek_spans order by trace_id;
+-- Check span order for sampled flag only
+select span_operation, parameters, lvl from peek_ordered_spans;
+-- Cleaning
+CALL clean_spans();
diff --git a/contrib/pg_tracing/sql/select.sql b/contrib/pg_tracing/sql/select.sql
new file mode 100644
index 0000000000..59277e1782
--- /dev/null
+++ b/contrib/pg_tracing/sql/select.sql
@@ -0,0 +1,103 @@
+-- Only trace queries with sample flag
+SET pg_tracing.sample_rate = 0.0;
+SET pg_tracing.caller_sample_rate = 1.0;
+
+-- Run a simple query
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ SELECT 1;
+-- Get top span id
+SELECT span_id AS top_span_id from pg_tracing_peek_spans where parent_id='0000000000000001' and span_type!='Parse' \gset
+-- Check parameters
+SELECT parameters from pg_tracing_peek_spans where span_id=:'top_span_id';
+-- Check the number of children
+SELECT count(*) from pg_tracing_peek_spans where parent_id=:'top_span_id';
+-- Check span_operation
+SELECT span_type, span_operation from pg_tracing_peek_spans where trace_id='00000000000000000000000000000001' order by span_start, span_end desc;
+-- Check userid
+SELECT userid = (SELECT usesysid FROM pg_user WHERE usename = current_user) FROM pg_tracing_peek_spans GROUP BY userid;
+-- Check dbid
+SELECT dbid = (SELECT oid FROM pg_database WHERE datname = (SELECT current_database())) FROM pg_tracing_peek_spans GROUP BY dbid;
+
+-- Check count of query_id
+SELECT count(distinct query_id) from pg_tracing_consume_spans where trace_id='00000000000000000000000000000001';
+
+-- Get initial number of traces reported
+SELECT processed_traces from pg_tracing_info \gset
+
+-- Trace a statement with function call
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-01'*/ SELECT count(*) from current_database();
+-- Check the generated span span_type, span_operation and order of function call
+SELECT span_type, span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000003';
+
+-- Check expected reported number of trace
+SELECT processed_traces = :processed_traces + 1 from pg_tracing_info;
+
+-- Trace a more complex query with multiple function calls
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000004-0000000000000004-01'*/ SELECT s.relation_size + s.index_size
+FROM (SELECT
+      pg_relation_size(C.oid) as relation_size,
+      pg_indexes_size(C.oid) as index_size
+    FROM pg_class C) as s limit 1;
+-- Check the nested level of spans for a query with multiple function calls
+SELECT span_type, span_operation, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000004';
+
+-- Check that we're in a correct state after a timeout
+set statement_timeout=200;
+-- Trace query triggering a statement timeout
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000007-0000000000000007-01'*/ select * from pg_sleep(10);
+SELECT span_type, span_operation, sql_error_code, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000007';
+-- Cleanup statement setting
+set statement_timeout=0;
+
+-- Trace a working query after the timeout to check we're in a consistent state
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000008-0000000000000008-01'*/ select 1;
+-- Check the spans order and error code
+SELECT span_type, span_operation, sql_error_code, lvl FROM peek_ordered_spans where trace_id='00000000000000000000000000000008';
+
+-- Cleanup
+SET plan_cache_mode='auto';
+
+-- Run a statement with node not executed
+/*dddbs='postgres.db',traceparent='00-0000000000000000000000000000000b-000000000000000b-01'*/ select 1 limit 0;
+SELECT span_operation, parameters, lvl from peek_ordered_spans where trace_id='0000000000000000000000000000000b';
+
+-- Test multiple statements in a single query
+/*dddbs='postgres.db',traceparent='00-0000000000000000000000000000000c-000000000000000c-01'*/ select 1; select 2;
+SELECT span_operation, parameters, lvl from peek_ordered_spans where trace_id='0000000000000000000000000000000c';
+
+-- Check that parameters are not exported when disabled
+SET pg_tracing.export_parameters=false;
+/*dddbs='postgres.db',traceparent='00-0000000000000000000000000000000d-000000000000000d-01'*/ select 1, 2, 3;
+SELECT span_operation, parameters, lvl from peek_ordered_spans where trace_id='0000000000000000000000000000000d';
+SET pg_tracing.export_parameters=true;
+
+-- Check multi statement query
+CALL clean_spans();
+SET pg_tracing.sample_rate = 1.0;
+-- Force a multi-query statement with \;
+SELECT 1\; SELECT 1, 2;
+SELECT span_type, span_operation, parameters, lvl from peek_ordered_spans;
+CALL clean_spans();
+
+-- Check standalone trace
+SELECT 1;
+-- Make sure we have unique span ids
+SELECT count(span_id) from pg_tracing_consume_spans group by span_id;
+
+-- Trigger a planner error
+SELECT '\xDEADBEEF'::bytea::text::int;
+-- Check planner error
+SELECT span_type, span_operation, parameters, sql_error_code, lvl from peek_ordered_spans;
+CALL clean_spans();
+
+-- Check spans generated by lazy functions
+CREATE OR REPLACE FUNCTION lazy_function(IN anyarray, OUT x anyelement)
+    RETURNS SETOF anyelement
+    LANGUAGE sql
+    AS 'select * from pg_catalog.generate_series(array_lower($1, 1), array_upper($1, 1), 1)';
+SELECT lazy_function('{1,2,3,4}'::int[]) FROM (VALUES (1,2));
+-- Check lazy function spans
+SELECT span_type, span_operation, parameters, lvl from peek_ordered_spans;
+
+-- Cleanup
+SET pg_tracing.sample_rate = 0.0;
+CALL clean_spans();
diff --git a/contrib/pg_tracing/sql/subxact.sql b/contrib/pg_tracing/sql/subxact.sql
new file mode 100644
index 0000000000..239323a143
--- /dev/null
+++ b/contrib/pg_tracing/sql/subxact.sql
@@ -0,0 +1,18 @@
+-- Enable full sampling
+SET pg_tracing.sample_rate = 1.0;
+
+-- Start a transaction with subxaction
+BEGIN;
+SAVEPOINT s1;
+INSERT INTO pg_tracing_test VALUES(generate_series(1, 2), 'aaa');
+SAVEPOINT s2;
+INSERT INTO pg_tracing_test VALUES(generate_series(1, 2), 'aaa');
+SAVEPOINT s3;
+SELECT 1;
+COMMIT;
+
+-- Check that subxact_count is correctly reported
+select span_operation, parameters, subxact_count, lvl FROM peek_ordered_spans;
+
+-- Cleaning
+CALL clean_spans();
diff --git a/contrib/pg_tracing/sql/trigger.sql b/contrib/pg_tracing/sql/trigger.sql
new file mode 100644
index 0000000000..bc995f25aa
--- /dev/null
+++ b/contrib/pg_tracing/sql/trigger.sql
@@ -0,0 +1,171 @@
+-- Only trace queries with sample flag
+SET pg_tracing.sample_rate = 0.0;
+SET pg_tracing.caller_sample_rate = 1.0;
+
+-- Create test tables
+CREATE TABLE Employee (
+    EmployeeId INT NOT NULL,
+    LastName VARCHAR(20) NOT NULL,
+    CONSTRAINT PK_Employee PRIMARY KEY (EmployeeId));
+CREATE TABLE Employee_Audit (
+    EmployeeId INT NOT NULL,
+    LastName VARCHAR(20) NOT NULL,
+    EmpAdditionTime VARCHAR(20) NOT NULL);
+
+-- Create trigger functions
+CREATE OR REPLACE FUNCTION employee_insert_trigger_fnc()
+  RETURNS trigger AS
+$$
+BEGIN
+    INSERT INTO Employee_Audit (EmployeeId, LastName, EmpAdditionTime)
+         VALUES(NEW.EmployeeId,NEW.LastName,current_date);
+RETURN NEW;
+END;
+$$
+LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION before_trigger_fnc()
+  RETURNS trigger AS
+$$
+BEGIN
+    PERFORM 'SELECT 1';
+RETURN NEW;
+END;
+$$
+LANGUAGE plpgsql;
+
+
+-- Hook the insert trigger twice after each row
+CREATE TRIGGER employee_insert_trigger
+  AFTER INSERT
+  ON Employee
+  FOR EACH ROW
+  EXECUTE PROCEDURE employee_insert_trigger_fnc();
+
+CREATE TRIGGER employee_insert_trigger_2
+  AFTER INSERT
+  ON Employee
+  FOR EACH ROW
+  EXECUTE PROCEDURE employee_insert_trigger_fnc();
+
+
+-- Call insert
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ INSERT INTO Employee VALUES(10,'Adams');
+
+--
+-- +--------------------------------------------------------------------------------------------------------+
+-- | B: INSERT INTO Employee                                                                                |
+-- ++----------+-+--------------+---------------------------------------------------------------------------+
+--  |C: Planner| |D: ExecutorRun| G: ExecutorFinish                                                       |
+--  +----------+ +--------------+--+-----------------------------------+-+--------------------------------+
+--                                 |I: INSERT INTO Employee_Audit...   | |N: INSERT INTO Employee_Audit...|
+--                                 +-+----------++--------------------++ +--------------------------------+
+--                                   |J: Planner||K: ExecutorRun      |  |O: ExecutorRun |
+--                                   +----------++--------------------+  +---------------+
+
+-- Gather span_id, span start and span end of call with triggers
+SELECT span_id AS span_b_id,
+        get_span_start(span_start) as span_b_start,
+        get_span_end(span_start) as span_b_end
+		from pg_tracing_peek_spans where parent_id='0000000000000001' \gset
+SELECT span_id AS span_c_id,
+        get_span_start(span_start) as span_c_start,
+        get_span_end(span_start) as span_c_end
+		from pg_tracing_peek_spans where parent_id=:'span_b_id' and span_type='Planner' \gset
+SELECT span_id AS span_d_id,
+        get_span_start(span_start) as span_d_start,
+        get_span_end(span_start) as span_d_end
+		from pg_tracing_peek_spans where parent_id=:'span_b_id' and span_type='Executor' and span_operation='ExecutorRun' \gset
+
+-- Executor Finish, first trigger
+SELECT span_id AS span_g_id,
+        get_span_start(span_start) as span_g_start,
+        get_span_end(span_start) as span_g_end
+		from pg_tracing_peek_spans where parent_id=:'span_b_id' and span_type='Executor' and span_operation='ExecutorFinish' \gset
+SELECT span_id AS span_i_id,
+        get_span_start(span_start) as span_i_start,
+        get_span_end(span_start) as span_i_end
+		from pg_tracing_peek_spans where parent_id=:'span_g_id' and span_type='Insert query' limit 1 \gset
+SELECT span_id AS span_j_id,
+        get_span_start(span_start) as span_j_start,
+        get_span_end(span_start) as span_j_end
+		from pg_tracing_peek_spans where parent_id=:'span_i_id' and span_type='Planner' \gset
+SELECT span_id AS span_k_id,
+        get_span_start(span_start) as span_k_start,
+        get_span_end(span_start) as span_k_end
+		from pg_tracing_peek_spans where parent_id=:'span_i_id' and span_type='Executor' and span_operation='ExecutorRun' \gset
+
+-- Executor Finish, second trigger
+SELECT span_id AS span_n_id,
+        get_span_start(span_start) as span_n_start,
+        get_span_end(span_start) as span_n_end
+        from pg_tracing_peek_spans where parent_id=:'span_g_id' and span_type='Insert query' limit 1 offset 1 \gset
+SELECT span_id AS span_o_id,
+        get_span_start(span_start) as span_o_start,
+        get_span_end(span_start) as span_o_end
+        from pg_tracing_peek_spans where parent_id=:'span_n_id' and span_type='Executor' and span_operation='ExecutorRun' \gset
+
+-- Check that spans' start and end are within expection
+-- Planner
+SELECT :span_c_start >= :span_b_start as planner_starts_after_root_span;
+-- ExecutorRun
+SELECT :span_d_start >= :span_c_end as executor_run_starts_after_planner,
+       :span_d_end <= :span_g_start as executor_run_ends_before_finish;
+-- ExecutorFinish
+SELECT :span_g_start <= :span_i_start as executor_finish_starts_before_child,
+       :span_g_end >= :span_o_end as executor_finish_ends_after_child;
+-- First trigger
+SELECT :span_i_end >= :span_n_end as first_trigger_ends_after_child;
+-- Second trigger
+SELECT :span_o_start >= :span_i_end  as second_trigger_starts_after_first,
+       :span_o_end >= :span_o_end as second_trigger_ends_after_child,
+       :span_o_end <= :span_g_end as second_trigger_ends_before_executorfinish_end,
+       :span_n_end <= :span_g_end as second_trigger_ends_before_root_executorfinish_end;
+
+-- Check span_operation
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000001';
+
+-- Check count of query_id
+SELECT count(distinct query_id) from pg_tracing_consume_spans where trace_id='00000000000000000000000000000001';
+
+-- Test table to test before trigger
+CREATE TABLE before_trigger_table (a int);
+
+-- Hook the before trigger
+CREATE TRIGGER employee_insert_trigger
+  BEFORE INSERT
+  ON before_trigger_table
+  FOR EACH ROW
+  EXECUTE PROCEDURE before_trigger_fnc();
+
+CREATE TRIGGER employee_update_trigger
+  BEFORE UPDATE
+  ON before_trigger_table
+  FOR EACH ROW
+  EXECUTE PROCEDURE before_trigger_fnc();
+
+CREATE TRIGGER employee_delete_trigger
+  BEFORE DELETE
+  ON before_trigger_table
+  FOR EACH ROW
+  EXECUTE PROCEDURE before_trigger_fnc();
+
+-- Test explain on a before trigger
+-- This will go through ExecutorEnd without any parent executor run
+/*dddbs='postgres.db',traceparent='00-fed00000000000000000000000000001-0000000000000003-01'*/ explain INSERT INTO before_trigger_table VALUES(10);
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='fed00000000000000000000000000001';
+
+-- Call before trigger insert
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ INSERT INTO before_trigger_table SELECT generate_series(1,2);
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000002';
+
+-- Call before trigger update
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-01'*/ UPDATE before_trigger_table set a=1;
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000003';
+
+-- Call before trigger delete
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000004-0000000000000004-01'*/ DELETE FROM before_trigger_table where a=1;
+SELECT span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000004';
+
+-- Check count of query_id
+SELECT count(distinct query_id) from pg_tracing_consume_spans;
diff --git a/contrib/pg_tracing/sql/utility.sql b/contrib/pg_tracing/sql/utility.sql
new file mode 100644
index 0000000000..118abe422f
--- /dev/null
+++ b/contrib/pg_tracing/sql/utility.sql
@@ -0,0 +1,189 @@
+-- Some helper functions
+CREATE OR REPLACE FUNCTION get_span_start(time_start timestamptz) RETURNS bigint AS
+$BODY$
+    SELECT extract(epoch from time_start);
+$BODY$
+LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION get_span_end(time_end timestamptz) RETURNS bigint AS
+$BODY$
+BEGIN
+    RETURN extract(epoch from time_end);
+END;
+$BODY$
+LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION get_span_end(time_end timestamptz) RETURNS bigint AS
+$BODY$
+BEGIN
+    RETURN extract(epoch from time_end);
+END;
+$BODY$
+LANGUAGE plpgsql;
+
+CREATE OR REPLACE PROCEDURE clean_spans() AS $$
+BEGIN
+    PERFORM count(*) from pg_tracing_consume_spans;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Create pg_tracing extension with sampling on
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ CREATE EXTENSION pg_tracing;
+
+-- This will create the following spans (non exhaustive list):
+--
+-- +-------------------------------------------------------------------------------------+
+-- | A: Utility: Create Extension                                                        |
+-- +-+-----------------------------------------------------------------------------------+
+--   +----------------------------------------------------------------------------------+
+--   |B: ProcessUtility: Create Extension                                               |
+--   +---+-----------------------------------+---+--------------------------------------+
+--       +-----------------------------------+   +-------------------------------------+
+--       |C: Utility: Create Function1       |   |E: Utility: Create Function2         |
+--       ++----------------------------------+   ++-----------------------------------++
+--        +----------------------------------+    +-----------------------------------+
+--        |D: ProcessUtility: Create Function|    |F: ProcessUtility: Create Function2|
+--        +----------------------------------+    +-----------------------------------+
+
+-- Extract span_ids, start and end of those spans
+SELECT span_id AS span_a_id,
+        get_span_start(span_start) as span_a_start,
+        get_span_end(span_start) as span_a_end
+		from pg_tracing_peek_spans where parent_id='0000000000000001' and span_type='Utility query' \gset
+
+SELECT span_id AS span_b_id,
+        get_span_start(span_start) as span_b_start,
+        get_span_end(span_start) as span_b_end
+		from pg_tracing_peek_spans where parent_id=:'span_a_id' and span_type='ProcessUtility' \gset
+
+SELECT span_id AS span_c_id,
+        get_span_start(span_start) as span_c_start,
+        get_span_end(span_start) as span_c_end
+		from pg_tracing_peek_spans where parent_id=:'span_b_id' and span_type='Utility query' limit 1 \gset
+
+SELECT span_id AS span_d_id,
+        get_span_start(span_start) as span_d_start,
+        get_span_end(span_start) as span_d_end
+		from pg_tracing_peek_spans where parent_id=:'span_c_id' and span_type='ProcessUtility' \gset
+
+SELECT span_id AS span_e_id,
+        get_span_start(span_start) as span_e_start,
+        get_span_end(span_start) as span_e_end
+		from pg_tracing_peek_spans where parent_id=:'span_b_id' and span_type='Utility query' limit 1 offset 1 \gset
+
+-- Check that the start and end of those spans are within expectation
+SELECT :span_a_start <= :span_b_start AS span_a_starts_first,
+		:span_a_end >= :span_b_end AS span_a_ends_last,
+
+		:span_d_end <= :span_c_end AS nested_span_ends_before_parent,
+		:span_c_end <= :span_e_start AS next_utility_starts_after;
+
+-- Clean current spans
+CALL clean_spans();
+
+--
+-- Test that no utility is captured with track_utility off
+--
+
+-- Set utility off
+SET pg_tracing.track_utility = off;
+
+-- Test utility tracking disabled + full sampling
+SET pg_tracing.sample_rate = 1.0;
+DROP EXTENSION pg_tracing;
+CREATE EXTENSION pg_tracing;
+SET pg_tracing.sample_rate = 0.0;
+
+-- View displaying spans with their nested level
+CREATE VIEW peek_spans_with_level AS
+    WITH RECURSIVE list_trace_spans(trace_id, parent_id, span_id, query_id, span_type, span_operation, span_start, span_end, sql_error_code, userid, dbid, pid, subxact_count, plan_startup_cost, plan_total_cost, plan_rows, plan_width, rows, nloops, shared_blks_hit, shared_blks_read, shared_blks_dirtied, shared_blks_written, local_blks_hit, local_blks_read, local_blks_dirtied, local_blks_written, blk_read_time, blk_write_time, temp_blks_read, temp_blks_written, temp_blk_read_time, temp_blk_write_time, wal_records, wal_fpi, wal_bytes, jit_functions, jit_generation_time, jit_inlining_time, jit_optimization_time, jit_emission_time, startup, parameters, lvl) AS (
+        SELECT p.*, 1
+        FROM pg_tracing_peek_spans p where not parent_id=ANY(SELECT span_id from pg_tracing_peek_spans)
+      UNION ALL
+        SELECT s.*, lvl + 1
+        FROM pg_tracing_peek_spans s, list_trace_spans st
+        WHERE s.parent_id = st.span_id
+    ) SELECT * FROM list_trace_spans;
+
+-- Create utility view to keep order stable
+CREATE VIEW peek_ordered_spans AS
+  SELECT * FROM peek_spans_with_level order by span_start, lvl, span_end;
+
+-- Nothing should have been generated
+select count(*) = 0 from pg_tracing_consume_spans;
+
+-- Prepare and execute a prepared statement
+PREPARE test_prepared_one_param (integer) AS SELECT $1;
+EXECUTE test_prepared_one_param(100);
+
+-- Nothing should be generated
+select count(*) = 0 from pg_tracing_consume_spans;
+
+-- Force a query to start from ExecutorRun
+SET plan_cache_mode='force_generic_plan';
+EXECUTE test_prepared_one_param(200);
+SET plan_cache_mode TO DEFAULT;
+
+-- Again, nothing should be generated
+select count(*) = 0 from pg_tracing_consume_spans;
+
+--
+-- Test that no utility is captured with track_utility off
+--
+
+-- Enable utility tracking and track everything
+SET pg_tracing.track_utility = on;
+SET pg_tracing.sample_rate = 1.0;
+
+-- Prepare and execute a prepared statement
+PREPARE test_prepared_one_param_2 (integer) AS SELECT $1;
+EXECUTE test_prepared_one_param_2(100);
+
+-- Check the number of generated spans
+select count(distinct(trace_id)) from pg_tracing_peek_spans;
+-- Check spans of test_prepared_one_param_2 execution
+select span_operation, parameters, lvl from peek_ordered_spans;
+-- Check the top span (standalone top span has trace_id=parent_id)
+select span_operation, parameters, lvl from peek_ordered_spans where right(trace_id, 16) = parent_id;
+CALL clean_spans();
+
+-- Test prepared statement with generic plan
+SET plan_cache_mode='force_generic_plan';
+EXECUTE test_prepared_one_param(200);
+SET plan_cache_mode TO DEFAULT;
+
+-- Check the number of generated spans
+select count(distinct(trace_id)) from pg_tracing_peek_spans;
+-- Check spans of test_prepared_one_param execution
+select span_operation, parameters, lvl from peek_ordered_spans;
+-- Check the top span (standalone top span has trace_id=parent_id)
+select span_operation, parameters, lvl from peek_ordered_spans where right(trace_id, 16) = parent_id;
+CALL clean_spans();
+
+-- Second create extension should generate an error that is captured by span
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ CREATE EXTENSION pg_tracing;
+select span_operation, parameters, sql_error_code, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000001';
+
+-- Create test table
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ CREATE TABLE pg_tracing_test (a int, b char(20));
+-- Check create table spans
+select trace_id, span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000002';
+
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-01'*/ CREATE INDEX pg_tracing_index ON pg_tracing_test (a);
+-- Check create index spans
+select trace_id, span_type, span_operation, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000003';
+
+CREATE OR REPLACE FUNCTION function_with_error(IN anyarray, OUT x anyelement, OUT n int)
+    RETURNS SETOF RECORD
+    LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE
+    AS 'select s from pg_catalog.generate_series(1, 1, 1) as g(s)';
+
+-- Check that tracing a function call with the wrong number of arguments is managed correctly
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000004-0000000000000004-01'*/ select function_with_error('{1,2,3}'::int[]);
+
+-- Check lazy function call with error
+select trace_id, span_type, span_operation, sql_error_code, lvl from peek_ordered_spans where trace_id='00000000000000000000000000000004';
+
+-- Cleanup
+CALL clean_spans();
+SET pg_tracing.track_utility TO DEFAULT;
diff --git a/contrib/pg_tracing/sql/wal.sql b/contrib/pg_tracing/sql/wal.sql
new file mode 100644
index 0000000000..17586627ae
--- /dev/null
+++ b/contrib/pg_tracing/sql/wal.sql
@@ -0,0 +1,14 @@
+-- Generate queries with wal write
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000001-0000000000000001-01'*/ INSERT INTO pg_tracing_test VALUES(generate_series(1, 10), 'aaa');
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000002-0000000000000002-01'*/ UPDATE pg_tracing_test SET b = 'bbb' WHERE a = 7;
+/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000003-0000000000000003-01'*/ DELETE FROM pg_tracing_test WHERE a = 9;
+
+-- Check WAL is generated for the above statements
+SELECT trace_id, span_type, span_operation,
+       wal_records > 0 as wal_records,
+       wal_bytes > 0 as wal_bytes
+FROM peek_ordered_spans;
+CALL clean_spans();
+
+-- Cleanup
+CALL clean_spans();
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index 44639a8dca..583b271c10 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -159,6 +159,7 @@ CREATE EXTENSION <replaceable>extension_name</replaceable>;
  &pgstatstatements;
  &pgstattuple;
  &pgsurgery;
+ &pgtracing;
  &pgtrgm;
  &pgvisibility;
  &pgwalinspect;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index e0dca81cb2..5708d64736 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -147,6 +147,7 @@
 <!ENTITY pgstatstatements SYSTEM "pgstatstatements.sgml">
 <!ENTITY pgstattuple     SYSTEM "pgstattuple.sgml">
 <!ENTITY pgsurgery       SYSTEM "pgsurgery.sgml">
+<!ENTITY pgtracing       SYSTEM "pgtracing.sgml">
 <!ENTITY pgtrgm          SYSTEM "pgtrgm.sgml">
 <!ENTITY pgvisibility    SYSTEM "pgvisibility.sgml">
 <!ENTITY pgwalinspect    SYSTEM "pgwalinspect.sgml">
diff --git a/doc/src/sgml/pgtracing.sgml b/doc/src/sgml/pgtracing.sgml
new file mode 100644
index 0000000000..8c5d74e62f
--- /dev/null
+++ b/doc/src/sgml/pgtracing.sgml
@@ -0,0 +1,872 @@
+<!-- doc/src/sgml/pgtracing.sgml -->
+
+<sect1 id="pgtracing" xreflabel="pg_tracing">
+ <title>pg_tracing &mdash; generate server-side spans for distributed tracing</title>
+
+ <indexterm zone="pgtracing">
+  <primary>pg_tracing</primary>
+ </indexterm>
+
+ <para>
+  The <filename>pg_tracing</filename> module provides a way to
+  generate server-side spans for distributed tracing.
+ </para>
+
+ <para>
+  The module must be loaded by adding <literal>pg_tracing</literal> to
+  <xref linkend="guc-shared-preload-libraries"/> in
+  <filename>postgresql.conf</filename> because it requires additional shared memory.
+  This means that a server restart is needed to add or remove the module.
+ </para>
+
+ <para>
+   When <filename>pg_tracing</filename> is active, it generates spans
+   on sampled queries. To access these spans, the module provides two views:
+   <structname>pg_tracing_consume_spans</structname> and
+   <structname>pg_tracing_peek_spans</structname>.
+   The utility functions <function>pg_tracing_reset</function> and
+   <function>pg_tracing_info</function> provide ways to read and reset
+   module's statistics. These are not available globally but
+   can be enabled for a specific database with
+   <command>CREATE EXTENSION pg_tracing</command>.
+ </para>
+
+ <sect2 id="pgtracing-pg-tracing-consume-spans">
+  <title>The <structname>pg_tracing_consume_spans</structname>/<structname>pg_tracing_peek_spans</structname> Views</title>
+
+  <para>
+   The spans generated by the module are made available via a
+   view named <structname>pg_tracing_consume_spans</structname>.
+   This view contains one row for each generated span.
+   The columns of the view are shown in <xref linkend="pgtracing-consume-spans-columns"/>.
+   The <structname>pg_tracing_peek_spans</structname> view
+   provides the same output as <structname>pg_tracing_consume_spans</structname>.
+   <structname>pg_tracing_peek_spans</structname> doesn't delete read spans.
+  </para>
+
+  <table id="pgtracing-consume-spans-columns">
+   <title><structname>pg_tracing_consume_spans</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>traceid</structfield> <type>char(32)</type>
+      </para>
+      <para>
+       Trace identifier of the span
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>parent_id</structfield> <type>char(16)</type>
+      </para>
+      <para>
+       Identifier of the span's parent
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>span_id</structfield> <type>char(16)</type>
+      </para>
+      <para>
+       Identifier of the span
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>queryid</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Hash code to identify identical normalized queries, 0 if query_id is disabled
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>span_type</structfield> <type>text</type>
+      </para>
+      <para>
+       Type of span (Planner, Executor...)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>span_operation</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the span's operation
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>span_start</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Start time of the span
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>span_end</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       End time of the span
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>sql_error_code</structfield> <type>bpchar</type>
+      </para>
+      <para>
+       Error code of the query. 00000 for a succesful query
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pid</structfield> <type>integer</type>
+      </para>
+      <para>
+       Pid of the backend process that processed the query
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>subxact_count</structfield> <type>smallint</type>
+      </para>
+      <para>
+       Active count of backend's subtransaction when the span was created
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>plan_startup_cost</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Estimated cost before fetching any tuples by the query planner
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>plan_total_cost</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Estimated total cost by the query planner
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>plan_rows</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Estimated number of row plan is expected to emit
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>plan_width</structfield> <type>integer</type>
+      </para>
+      <para>
+       Estimated row width in bytes by the query planner
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rows</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of tuples processed
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>nloops</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of cycles for this node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>shared_blks_hit</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of shared block cache hits by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>shared_blks_read</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of shared blocks read by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>shared_blks_dirtied</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of shared blocks dirtied by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>shared_blks_written</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of shared blocks written by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>local_blks_hit</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of local block cache hits by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>local_blks_read</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of local blocks read by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>local_blks_dirtied</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of local blocks dirtied by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>local_blks_written</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of local blocks written by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_read_time</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Time spent reading blocks in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_write_time</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Time spent writing blocks in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>temp_blks_read</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of temp blocks read by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>temp_blks_written</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of temp blocks written by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>temp_blk_read_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Total time the node spent reading temporary file blocks, in
+       milliseconds (if <xref linkend="guc-track-io-timing"/> is enabled,
+       otherwise zero)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>temp_blk_write_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Total time the node spent writing temporary file blocks, in
+       milliseconds (if <xref linkend="guc-track-io-timing"/> is enabled,
+       otherwise zero)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_records</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of WAL records generated by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_fpi</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of WAL full page images generated by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_bytes</structfield> <type>numeric</type>
+      </para>
+      <para>
+       Total amount of WAL generated by the node in bytes
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>jit_functions</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of functions JIT-compiled by the node
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>jit_generation_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Total time spent by the node on generating JIT code, in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>jit_inlining_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Total time spent by the node on inlining functions, in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>jit_optimization_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Total time spent by the node on optimizing, in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>startup</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Time to the first tuple in nanoseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>parameters</structfield> <type>text</type>
+      </para>
+      <para>
+       Value of the query's parameters
+      </para></entry>
+     </row>
+
+    </tbody>
+   </tgroup>
+  </table>
+
+  <para>
+   For security reasons, only superusers and roles with privileges of the
+   <literal>pg_read_all_stats</literal> role are allowed to access the
+   <structname>pg_tracing_peek_spans</structname> and
+   <structname>pg_tracing_consume_spans</structname>.
+  </para>
+
+ </sect2>
+
+ <sect2 id="pgtracing-pg-tracing-info">
+  <title>The <structname>pg_tracing_info</structname> View</title>
+
+  <indexterm>
+   <primary>pg_tracing_info</primary>
+  </indexterm>
+
+  <para>
+   The statistics of the <filename>pg_tracing</filename> module
+   itself are tracked and made available via a view named
+   <structname>pg_tracing_info</structname>.  This view contains
+   only a single row.  The columns of the view are shown in
+   <xref linkend="pgtracinginfo-columns"/>.
+  </para>
+
+  <table id="pgtracinginfo-columns">
+   <title><structname>pg_tracing_info</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>processed_traces</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number traces captured by the module
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>processed_spans</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number spans captured by the module
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dropped_traces</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of sampled queries dropped because shared buffer
+       was full
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>dropped_spans</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of spans dropped because more than
+       <varname>pg_tracing.max_span</varname> were stored
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_consume</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Time at which spans were consumed
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>stats_reset</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Time at which all statistics in the
+       <structname>pg_tracing_info</structname> view were last reset.
+      </para></entry>
+     </row>
+
+    </tbody>
+   </tgroup>
+  </table>
+ </sect2>
+
+ <sect2 id="pgtracing-funcs">
+  <title>Functions</title>
+
+  <variablelist>
+   <varlistentry>
+    <term>
+     <function>pg_tracing_reset() returns void</function>
+     <indexterm>
+      <primary>pg_tracing_reset</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      <function>pg_tracing_reset</function> discards all statistics
+      gathered so far by <filename>pg_tracing</filename>.
+      Span buffer is not emptied by this function.
+      By default, this function can only be executed by superusers.
+      Access may be granted to others using <command>GRANT</command>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <function>pg_tracing_spans(consume boolean) returns setof record</function>
+     <indexterm>
+      <primary>pg_tracing</primary>
+      <secondary>function</secondary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      The <structname>pg_tracing_consume_spans</structname> and
+      <structname>pg_tracing_peek_spans</structname> views are defined in
+      terms of a function named <function>pg_tracing_spans</function>.
+      It is possible for clients to call the
+      <function>pg_tracing_spans</function> function directly. By
+      specifying <literal>consume := false</literal>, spans won't be removed
+      from the span buffer. With <literal>consume := true</literal>,
+      read spans will be deleted.
+      By default, this function can only be executed by superusers.
+      Access may be granted to others using <command>GRANT</command>.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </sect2>
+
+ <sect2 id="pgtracing-config-params">
+  <title>Configuration Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term>
+     <varname>pg_tracing.max_span</varname> (<type>integer</type>)
+     <indexterm>
+      <primary><varname>pg_tracing.max_span</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      <varname>pg_tracing.max_span</varname> is the maximum number of
+      spans stored by the module (i.e., the maximum number of rows
+      returned by the <structname>pg_tracing_spans</structname> function).
+      If more spans are observed, the span buffer will be emptied if
+      <varname>pg_tracing.buffer_mode</varname> is <literal>drop_on_full</literal>. If
+      <varname>pg_tracing.buffer_mode</varname> is <literal>keep_on_full</literal>,
+      the new spans will be dropped and tracing will be aborted. The default value
+      is 5000. This parameter can only be set at server start.
+
+     <note>
+
+       <para>
+        A span takes around 370 bytes of memory so 5000 spans will use
+        1.7MB while 10000 spans will use 3.5MB. You can use the following query
+        to get the exact size used by the pg_tracing's spans:
+       </para>
+
+<programlisting>
+SELECT
+    (SELECT setting from pg_settings where name='pg_tracing.max_span') as max_span,
+    pg_size_pretty(size) as total_span_size,
+    pg_size_pretty(size/(SELECT setting from pg_settings where name='pg_tracing.max_span')::int) as size_per_span
+FROM pg_shmem_allocations
+WHERE name ='PgTracing Spans';
+</programlisting>
+
+     </note>
+
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>pg_tracing.buffer_mode</varname> (<type>enum</type>)
+     <indexterm>
+      <primary><varname>pg_tracing.buffer_mode</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      <varname>pg_tracing.buffer_mode</varname> controls span buffer's
+      behaviour when <varname>pg_tracing.max_span</varname> spans is reached.
+      If <literal>keep_on_full</literal>, existing buffer is kept while new spans are dropped.
+      If <literal>drop_on_full</literal>, existing buffer is dropped and new spans are added.
+      The default value is <literal>keep_on_full</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>pg_tracing.max_parameter_size</varname> (<type>integer</type>)
+     <indexterm>
+      <primary><varname>pg_tracing.max_parameter_size</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      <varname>pg_tracing.max_parameter_size</varname> controls the maximum
+      size of the parameter string.
+      The default value is <literal>1024</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>pg_tracing.track</varname> (<type>enum</type>)
+     <indexterm>
+      <primary><varname>pg_tracing.track</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      <varname>pg_tracing.track</varname> controls which statements
+      should spans should be generated.
+      Specify <literal>top</literal> to only generate span for top-level
+      statements (those issued directly by clients), <literal>all</literal>
+      to also generate spans for nested statements (such as statements invoked
+      within functions), or <literal>none</literal> to disable span generation.
+      The default value is <literal>all</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>pg_tracing.track_utility</varname> (<type>boolean</type>)
+     <indexterm>
+      <primary><varname>pg_tracing.track_utility</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      <varname>pg_tracing.track_utility</varname> controls whether spans
+      should be generated for utility statements. Utility commands are
+      all those other than <command>SELECT</command>, <command>INSERT</command>,
+      <command>UPDATE</command>, <command>DELETE</command>, <command>TABLE</command>,
+      and <command>MERGE</command>. The default value is <literal>on</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>pg_tracing.sample_rate</varname> (<type>real</type>)
+     <indexterm>
+      <primary><varname>pg_tracing.sample_rate</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      <varname>pg_tracing.sample_rate</varname> controls the fraction of
+      statements that generate spans. Statements with tracecontext propagated
+      with SQLCommenter and sampled flag enabled are not impacted by this parameter.
+      In case of nested statements, either all will be explained or none.
+      The default value is <literal>0</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>pg_tracing.caller_sample_rate</varname> (<type>real</type>)
+     <indexterm>
+      <primary><varname>pg_tracing.caller_sample_rate</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      <varname>pg_tracing.caller_sample_rate</varname> controls the fraction of
+      statements with SQLCommenter tracecontext and an enabled sampled flag that
+      will generate spans. The default value is <literal>1</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>pg_tracing.export_parameters</varname> (<type>boolean</type>)
+     <indexterm>
+      <primary><varname>pg_tracing.export_parameters</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      <varname>pg_tracing.export_parameters</varname> controls whether
+      query's parameters should be exported in spans metadata.
+      The default value is <literal>on</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+
+  <para>
+   The module requires additional shared memory proportional to
+   <varname>pg_tracing.max_span</varname>.  Note that this
+   memory is consumed whenever the module is loaded, even if
+   no spans are generated.
+  </para>
+
+  <para>
+   These parameters must be set in <filename>postgresql.conf</filename>.
+   Typical usage might be:
+
+<programlisting>
+# postgresql.conf
+shared_preload_libraries = 'pg_tracing'
+
+compute_query_id = on
+pg_tracing.span_max = 10000
+pg_tracing.track = all
+</programlisting>
+  </para>
+ </sect2>
+
+ <sect2 id="pgtracing-implementation-details">
+  <title>Implementation Details</title>
+
+ <sect3 id="pgtracing-sqlcommenter">
+  <title>SQLCommenter</title>
+  <para>
+   A query with <ulink url="https://google.github.io/sqlcommenter/">SQLcommenter</ulink> can propagate a trace context to the database. It will have the following format: <literal>/*dddbs='postgres.db',traceparent='00-00000000000000000000000000000009-0000000000000005-01'*/ select 1;</literal>. The traceparent fields are detailed in <ulink url="https://www.w3.org/TR/trace-context/#traceparent-header-field-values">w3c's trace-context</ulink>
+   <itemizedlist spacing="compact">
+    <listitem>
+     <para>
+      <literal>00000000000000000000000000000009</literal>: trace id
+     </para>
+    </listitem>
+    <listitem>
+     <para>
+      <literal>0000000000000005</literal>: parent id
+     </para>
+    </listitem>
+    <listitem>
+     <para>
+     <literal>01</literal>: trace flags (<literal>01 == sampled</literal>)
+     </para>
+    </listitem>
+   </itemizedlist>
+   When a query with SQLCommenter is detected, the trace context is extracted and used by <literal>pg_tracing</literal>.
+  </para>
+ </sect3>
+
+ <sect3 id="pgtracing-sampling-decision">
+  <title>Sampling Decision</title>
+  <para>
+  Spans will only be generated for sampled queries. A query is sampled if:
+  <itemizedlist spacing="compact">
+   <listitem>
+    <para>
+     It has a trace context propagated throught SQLCommenter with the sampled flag enabled and it passes the <varname>pg_tracing.caller_sample_rate</varname>
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    It has no SQLCommenter but the query randomly passes the global <varname>pg_tracing.sample_rate</varname>
+    </para>
+   </listitem>
+  </itemizedlist>
+   By default, <literal>pg_tracing.caller_sample_rate = 1</literal> and <literal>pg_tracing.sample_rate = 0</literal>.
+   Only queries with a trace context and a <literal>sampled := 01</literal> will be sampled, effectively leaving sampling decision to the callers.
+  </para>
+ </sect3>
+
+ <sect3 id="pgtracing-query-error">
+  <title>Query error</title>
+  <para>
+   On query error (statement timeout reached, query canceled...), a sampled
+   query will still generate spans. The planstate will be partial so
+   the generated spans only reflect nodes that were executed and it's
+   possible that some part of the plan will be missing.
+  </para>
+ </sect3>
+
+ </sect2>
+
+ <sect2 id="pgtracing-sample-output">
+  <title>Sample Output</title>
+
+<screen>
+=# SET pg_tracing.sample_rate = 1.0;
+
+=# SELECT 1;
+
+=# select trace_id, parent_id, span_id, span_start, span_end, span_type, span_operation from pg_tracing_consume_spans order by span_start;
+             trace_id             |    parent_id     |     span_id      |          span_start           |           span_end            |  span_type   | span_operation
+----------------------------------+------------------+------------------+-------------------------------+-------------------------------+--------------+----------------
+ 458fbefd7034e670eb3d9c930862c378 | eb3d9c930862c378 | bdecb6e35d429f3d | 2024-01-10 09:54:16.321253+00 | 2024-01-10 09:54:16.321587+00 | Select query | SELECT $1;
+ 458fbefd7034e670eb3d9c930862c378 | bdecb6e35d429f3d | ad49f27543b0175d | 2024-01-10 09:54:16.3213+00   | 2024-01-10 09:54:16.321412+00 | Planner      | Planner
+ 458fbefd7034e670eb3d9c930862c378 | bdecb6e35d429f3d | 8805f7749249536b | 2024-01-10 09:54:16.321485+00 | 2024-01-10 09:54:16.321529+00 | Executor     | ExecutorRun
+
+=# select trace_id, parent_id, span_id, span_start, span_end, span_type, span_operation from pg_tracing_consume_spans order by span_start;
+ trace_id | parent_id | span_id | span_start | duration | name | resource
+----------+-----------+---------+------------+----------+------+----------
+(0 rows)
+
+</screen>
+ </sect2>
+
+ <sect2 id="pgtracing-authors">
+  <title>Authors</title>
+
+  <para>
+   Anthonin Bonnefoy <email>anthonin.bonnefoy@datadoghq.com</email>.
+  </para>
+ </sect2>
+
+</sect1>
-- 
2.39.3 (Apple Git-146)

