From beff2f5d3568dbf44e63a14e6130c65880328112 Mon Sep 17 00:00:00 2001
From: Anthonin Bonnefoy <anthonin.bonnefoy@datadoghq.com>
Date: Fri, 26 Jun 2026 11:06:16 +0200
Subject: Add PGTRACE env to enable protocol tracing in libpq

Add 3 env vars, PGTRACE, PGTRACEAPPEND and PGTRACEFLAGS. When PGTRACE is
set, PQtrace will be set when connection options are processed. This
allows to trace protocol messages like startup messages and
BackendKeyData. This also allows enabling protocol tracing on any
application using libpq.
---
 doc/src/sgml/libpq.sgml           | 70 +++++++++++++++++++++++++++++++
 src/interfaces/libpq/fe-connect.c | 60 +++++++++++++++++++++++++-
 src/interfaces/libpq/libpq-int.h  |  5 +++
 3 files changed, 134 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 7d3c3bb66d8..4d4bd4ef559 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2424,6 +2424,46 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-trace-file" xreflabel="trace_file">
+      <term><literal>trace_file</literal></term>
+      <listitem>
+       <para>
+        If set, enables tracing of the client/server protocol exchange to
+        the named file. The special value <literal>-</literal> sends the
+        trace to <literal>stdout</literal>. Multiple connections using
+        the same trace file is currently not supported.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-trace-flags" xreflabel="trace_flags">
+      <term><literal>trace_flags</literal></term>
+      <listitem>
+       <para>
+        Specifies the trace flags applied when
+        <xref linkend="libpq-connect-trace-file"/> is set.  The value is
+        an integer that is the bitwise OR of the flag values accepted by
+        <xref linkend="libpq-PQsetTraceFlags"/>:
+        <literal>1</literal> for <literal>PQTRACE_SUPPRESS_TIMESTAMPS</literal>,
+        <literal>2</literal> for <literal>PQTRACE_REGRESS_MODE</literal>.
+        The default is <literal>0</literal>.  This option has no effect
+        unless <literal>trace_file</literal> is also set.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-trace-file-append" xreflabel="trace_file_append">
+      <term><literal>trace_file_append</literal></term>
+      <listitem>
+       <para>
+        If set to <literal>1</literal>, the trace file specified by <xref
+        linkend="libpq-connect-trace-file"/> will be opened in append mode.
+        With the default <literal>0</literal> value, the file will be opened in
+        write mode. This option has no effect unless
+        <literal>trace_file</literal> is also set. </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-load-balance-hosts" xreflabel="load_balance_hosts">
       <term><literal>load_balance_hosts</literal></term>
       <listitem>
@@ -9454,6 +9494,36 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
       linkend="libpq-connect-oauth-ca-file"/> connection parameter.
      </para>
     </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGTRACE</envar></primary>
+      </indexterm>
+      <envar>PGTRACE</envar> behaves the same as the <xref
+      linkend="libpq-connect-trace-file"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGTRACEFLAGS</envar></primary>
+      </indexterm>
+      <envar>PGTRACEFLAGS</envar> behaves the same as the <xref
+      linkend="libpq-connect-trace-flags"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGTRACEAPPEND</envar></primary>
+      </indexterm>
+      <envar>PGTRACEAPPEND</envar> behaves the same as the <xref
+      linkend="libpq-connect-trace-file-append"/> connection parameter.
+     </para>
+    </listitem>
    </itemizedlist>
   </para>
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 38422becc48..d7f80f80631 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -385,6 +385,19 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"trace_file", "PGTRACE",
+		NULL, NULL,
+		"Trace-File", "D", 64,
+	offsetof(struct pg_conn, trace_file_str)},
+
+	{"trace_file_append", "PGTRACEAPPEND", "0", NULL,
+		"Trace-File-Append", "", 1,
+	offsetof(struct pg_conn, trace_file_append)},
+
+	{"trace_flags", "PGTRACEFLAGS", NULL, NULL,
+		"Trace-Flags", "D", 1,
+	offsetof(struct pg_conn, trace_flags_str)},
+
 	{"load_balance_hosts", "PGLOADBALANCEHOSTS",
 		DefaultLoadBalanceHosts, NULL,
 		"Load-Balance-Hosts", "", 8,	/* sizeof("disable") = 8 */
@@ -1257,6 +1270,7 @@ bool
 pqConnectOptions2(PGconn *conn)
 {
 	int			i;
+	int			trace_flags = 0;
 
 	/*
 	 * Allocate memory for details about each host to which we might possibly
@@ -2057,6 +2071,39 @@ pqConnectOptions2(PGconn *conn)
 		conn->scram_client_key_len = len;
 	}
 
+	/* If we already have an opened trace file, keep it  */
+	if (conn->trace_file_str && conn->trace_file == NULL)
+	{
+		if (strcmp(conn->trace_file_str, "-") == 0)
+			conn->trace_file = stdout;
+		else
+		{
+			char	   *mode = (conn->trace_file_append && conn->trace_file_append[0] == '1') ? "a" : "w";
+
+			conn->trace_file = fopen(conn->trace_file_str, mode);
+		}
+		if (conn->trace_file == NULL)
+		{
+			libpq_append_conn_error(conn, "could not open trace file \"%s\": %m", conn->trace_file_str);
+			return false;
+		}
+		/* Make it line-buffered */
+		if (conn->trace_file != stdout)
+			setvbuf(conn->trace_file, NULL, PG_IOLBF, 0);
+		PQtrace(conn, conn->trace_file);
+
+		if (conn->trace_flags_str)
+		{
+			if (!pqParseIntParam(conn->trace_flags_str, &trace_flags, conn,
+								 "trace_flags"))
+			{
+				return false;
+			}
+		}
+
+		PQsetTraceFlags(conn, trace_flags);
+	}
+
 	if (conn->scram_server_key)
 	{
 		int			len;
@@ -5043,6 +5090,7 @@ pqMakeEmptyPGconn(void)
 	conn->sock = PGINVALID_SOCKET;
 	conn->altsock = PGINVALID_SOCKET;
 	conn->Pfdebug = NULL;
+	conn->trace_file = NULL;
 
 	/*
 	 * We try to send at least 8K at a time, which is the usual size of pipe
@@ -5164,7 +5212,17 @@ freePGconn(PGconn *conn)
 	free(conn->oauth_client_secret);
 	free(conn->oauth_ca_file);
 	free(conn->oauth_scope);
-	/* Note that conn->Pfdebug is not ours to close or free */
+	free(conn->trace_file_str);
+	free(conn->trace_file_append);
+	free(conn->trace_flags_str);
+
+	/*
+	 * Note that conn->Pfdebug is not ours to close or free if provided
+	 * externally. If tracing was enabled through PGTRACE, conn->Pfdebug will
+	 * be set to conn->trace_file, so closing conn->trace_file is enough.
+	 */
+	if (conn->trace_file && conn->trace_file != stdout)
+		fclose(conn->trace_file);
 	free(conn->events);
 	pqReleaseConnHosts(conn);
 	free(conn->connip);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 461b39620c3..573a64c54a8 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -449,6 +449,11 @@ struct pg_conn
 	char	   *oauth_ca_file;	/* CA file path */
 	bool		oauth_want_retry;	/* should we retry on failure? */
 
+	FILE	   *trace_file;
+	char	   *trace_file_str;
+	char	   *trace_file_append;	/* Trace opened in append mode (0 or 1) */
+	char	   *trace_flags_str;
+
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
 	int			traceFlags;
-- 
2.54.0

