Below, I propose a solution, built with the e-maj software (a free, open source PostgreSQL extension), which helps here. It is a set of triggers which log all changes made to a database (or a subset of it), and store this in a separate schema in very detailed audit logs, on which you can make your SELECTs.
What is logged is detailed at:
https://emaj.readthedocs.io/en/latest/architecture.html#logged-sql-statements
E-Maj is available via the PostgreSQL Extension network (https://pgxn.org/dist/e-maj/).
If you log "myschema.mytable", e-maj will create a table "emaj_myschema.mytable_log". Each updates will be logged as two rows (old values, new values, ID and time of change...), as follows:
select * from emaj_myschema.mytable_log;

But this is only the beginning. From this, you can do:
CREATE EXTENSION IF NOT EXISTS hstore;
-- This first json function was created by klin, https://stackoverflow.com/questions/34746807/migrate-json-to-hstore
create or replace function simple_jsonb_to_hstore(jdata jsonb)
returns hstore language sql immutable
as $$
select hstore(array_agg(key), array_agg(value))
from jsonb_each_text(jdata)
$$;
A query getting something close to what you wanted:
with newr AS (
select emaj_gid, emaj_tuple, emaj_verb,
simple_jsonb_to_hstore(to_jsonb(t1)
) rec_hstore
from ( select * from emaj_myschema.mytable_log) t1
WHERE emaj_verb='UPD' AND emaj_tuple='NEW')
select emaj_changed, emaj_txid, emaj_user,
simple_jsonb_to_hstore(to_jsonb(oldr) )
- newr.rec_hstore
- ARRAY['emaj_tuple','emaj_changed'] as oldval,
newr.rec_hstore
- simple_jsonb_to_hstore(to_jsonb(oldr) )
- ARRAY['emaj_tuple','emaj_changed'] as newval
FROM ( select * from emaj_myschema.mytable_log) oldr
JOIN newr ON oldr.emaj_gid = newr.emaj_gid
WHERE oldr.emaj_verb='UPD' AND oldr.emaj_tuple='OLD'
;
Result such as below:

If you frequently do this, create a function:
DROP FUNCTION IF EXISTS emaj.logs_of_changed_fields;
CREATE FUNCTION emaj.logs_of_changed_fields(schema_name text, table_name text)
RETURNS TABLE (
emaj_changed timestamptz,
emaj_txid bigint,
emaj_user varchar,
oldval hstore, -- only the fields that changed, old values
newval hstore,-- only the fields that changed, new values
newrecord hstore -- new values for all fields (including unchanged ones)
)
AS $$
BEGIN
RETURN QUERY EXECUTE '
with newr AS (
select emaj_gid, emaj_tuple, emaj_verb,
simple_jsonb_to_hstore(to_jsonb(t1)
) rec_hstore
from ( select * from emaj_' || schema_name || '.' || table_name || '_log ) t1
WHERE emaj_verb=''UPD'' AND emaj_tuple=''NEW''
)
select emaj_changed, emaj_txid, emaj_user,
simple_jsonb_to_hstore(to_jsonb(oldr) )
- newr.rec_hstore
- ARRAY[''emaj_tuple'',''emaj_changed''] as oldval,
newr.rec_hstore
- simple_jsonb_to_hstore(to_jsonb(oldr) )
- ARRAY[''emaj_tuple'', ''emaj_changed''] as newval,
newr.rec_hstore as newrecord
FROM ( select * from emaj_' || schema_name || '.' || table_name || '_log ) oldr
JOIN newr ON oldr.emaj_gid = newr.emaj_gid
WHERE oldr.emaj_verb=''UPD'' AND oldr.emaj_tuple=''OLD''
';
END;
$$ LANGUAGE PLPGSQL;
This will show you all updated fields, including when several fields were updated at the same time. If you want to further analyze updates to a single field, do:
select oldval -> 'field_1' AS old_value,
newval -> 'field_1' as new_value,
* from emaj.logs_of_changed_fields('exo_public','t_acteur');
Further, the result from the SQL below has structure which is precisely that of your example:
select newrecord -> 'user_field' as user_field, (old).key,
(old).value as old_value, (new).value as new_value
from (
select newrecord,
each(oldval) as old
,each(newval) as new
from emaj.logs_of_changed_fields('exo_public','t_acteur')
) t;
Result as below (the 'key' field is the key of the hstore, that is your 'field_changed'):
