MINOR: log: Add new "error-log-format" option

This option can be used to define a specific log format that will be
used in case of error, timeout, connection failure on a frontend... It
will be used for any log line concerned by the log-separate-errors
option. It will also replace the format of specific error messages
decribed in section 8.2.6.
If no "error-log-format" is defined, the legacy error messages are still
emitted and the other error logs keep using the regular log-format.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 428339a..182ecc6 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -3561,6 +3561,7 @@
 errorloc302                               X          X         X         X
 -- keyword -------------------------- defaults - frontend - listen -- backend -
 errorloc303                               X          X         X         X
+error-log-format                          X          X         X         -
 force-persist                             -          -         X         X
 filter                                    -          X         X         X
 fullconn                                  X          -         X         X
@@ -4943,6 +4944,24 @@
              "email-alert myhostname", section 3.6 about mailers.
 
 
+error-log-format <string>
+  Specifies the log format string to use in case of connection error on the frontend side.
+  May be used in sections:    defaults | frontend | listen | backend
+                                 yes   |    yes   |   yes  |    no
+
+  This directive specifies the log format string that will be used for logs
+  containing information related to errors, timeouts, retries redispatches or
+  HTTP status code 5xx. This format will in short be used for every log line
+  that would be concerned by the "log-separate-errors" option, including
+  connection errors described in section 8.2.6..
+  If the directive is used in a defaults section, all subsequent frontends will
+  use the same log format. Please see section 8.2.4 which covers the log format
+  string in depth.
+
+  "error-log-format" directive overrides previous "error-log-format"
+  directives.
+
+
 force-persist { if | unless } <condition>
   Declare a condition to force persistence on down servers
   May be used in sections:    defaults | frontend | listen | backend
@@ -7823,6 +7842,8 @@
   directive is used in a defaults section, all subsequent frontends will use
   the same log format. Please see section 8.2.4 which covers the log format
   string in depth.
+  A specific log-format used only in case of connection error can also be
+  defined, see the "error-log-format" option.
 
   "log-format" directive overrides previous "option tcplog", "log-format",
   "option httplog" and "option httpslog" directives.
@@ -21107,13 +21128,14 @@
 -----------------------
 
 When an incoming connection fails due to an SSL handshake or an invalid PROXY
-protocol header, HAProxy will log the event using a shorter, fixed line format.
+protocol header, HAProxy will log the event using a shorter, fixed line format,
+unless a dedicated error log format is defined through an "error-log-format"
+line. In the latter case, the legacy log format described below will not be
+used anymore, and all error log lines will follow the defined format.
 By default, logs are emitted at the LOG_INFO level, unless the option
 "log-separate-errors" is set in the backend, in which case the LOG_ERR level
 will be used. Connections on which no data are exchanged (e.g. probes) are not
-logged if the "dontlognull" option is set. If the "log-error-via-logformat" option
-is set, those messages are not emitted and a line following the configured
-log-format is emitted instead.
+logged if the "dontlognull" option is set.
 
 The format looks like this :
 
diff --git a/include/haproxy/proxy-t.h b/include/haproxy/proxy-t.h
index a34752b..38a6ca9 100644
--- a/include/haproxy/proxy-t.h
+++ b/include/haproxy/proxy-t.h
@@ -371,6 +371,7 @@
 	struct list logsrvs;
 	struct list logformat; 			/* log_format linked list */
 	struct list logformat_sd;		/* log_format linked list for the RFC5424 structured-data part */
+	struct list logformat_error;		/* log_format linked list used in case of connection error on the frontend */
 	struct buffer log_tag;                   /* override default syslog tag */
 	struct ist header_unique_id; 		/* unique-id header */
 	struct list format_unique_id;		/* unique-id format */
@@ -429,6 +430,9 @@
 		char *logformat_sd_string;	/* log format string for the RFC5424 structured-data part */
 		char *lfsd_file;		/* file name where the structured-data logformat string for RFC5424 appears (strdup) */
 		int  lfsd_line;			/* file name where the structured-data logformat string for RFC5424 appears */
+		char *error_logformat_string;
+		char *elfs_file;
+		int elfs_line;
 	} conf;					/* config information */
 	struct eb_root used_server_addr;        /* list of server addresses in use */
 	void *parent;				/* parent of the proxy when applicable */
diff --git a/reg-tests/ssl/ssl_errors.vtc b/reg-tests/ssl/ssl_errors.vtc
index d8fea43..2eaf1ff 100644
--- a/reg-tests/ssl/ssl_errors.vtc
+++ b/reg-tests/ssl/ssl_errors.vtc
@@ -44,12 +44,12 @@
     barrier b1 sync
 
     recv
-    expect ~ ".*conn_status:\"30:SSL client CA chain cannot be verified\" hsk_err:\"337100934:error:1417C086:SSL routines:tls_process_client_certificate:certificate verify failed\" CN=\"/C=FR/O=HAProxy Technologies/CN=Client\",serial=1007,hash=063DCC2E6A9159E66994B325D6D2EF3D17A75B6F"
+    expect ~ "ERROR.*conn_status:\"30:SSL client CA chain cannot be verified\" hsk_err:\"337100934:error:1417C086:SSL routines:tls_process_client_certificate:certificate verify failed\" CN=\"/C=FR/O=HAProxy Technologies/CN=Client\",serial=1007,hash=063DCC2E6A9159E66994B325D6D2EF3D17A75B6F"
 
     barrier b1 sync
 
     recv
-    expect ~ ".*conn_status:\"31:SSL client certificate not trusted\" hsk_err:\"337100934:error:1417C086:SSL routines:tls_process_client_certificate:certificate verify failed\" CN=\"/C=FR/O=HAProxy Technologies/CN=Client\",serial=1007,hash=063DCC2E6A9159E66994B325D6D2EF3D17A75B6F"
+    expect ~ "ERROR.*conn_status:\"31:SSL client certificate not trusted\" hsk_err:\"337100934:error:1417C086:SSL routines:tls_process_client_certificate:certificate verify failed\" CN=\"/C=FR/O=HAProxy Technologies/CN=Client\",serial=1007,hash=063DCC2E6A9159E66994B325D6D2EF3D17A75B6F"
 
     barrier b1 sync
 
@@ -57,7 +57,7 @@
     # the client certificate chain is never parsed and verified so we can't
     # have information about the client's certificate.
     recv
-    expect ~ ".*conn_status:\"34:SSL handshake failure\" hsk_err:\"337678529:error:142090C1:SSL routines:tls_early_post_process_client_hello:no shared cipher\" CN=\"\",serial=-,hash=-"
+    expect ~ "ERROR.*conn_status:\"34:SSL handshake failure\" hsk_err:\"337678529:error:142090C1:SSL routines:tls_early_post_process_client_hello:no shared cipher\" CN=\"\",serial=-,hash=-"
 } -start
 
 syslog Slg_https_fmt -level info {
@@ -69,17 +69,17 @@
 
 syslog Slg_https_fmt_err -level info {
     recv
-    expect ~ ".*https_logfmt_ssl_lst~ https_logfmt_ssl_lst/<NOSRV>.*30/000000001417C086/0/2 TLSv1.3/TLS_AES_256_GCM_SHA384"
+    expect ~ "ERROR.*https_logfmt_ssl_lst~ https_logfmt_ssl_lst/<NOSRV>.*30/000000001417C086/0/2 TLSv1.3/TLS_AES_256_GCM_SHA384"
 
     barrier b1 sync
 
     recv
-    expect ~ ".*https_logfmt_ssl_lst~ https_logfmt_ssl_lst/<NOSRV>.*31/000000001417C086/20/0 TLSv1.3/TLS_AES_256_GCM_SHA384"
+    expect ~ "ERROR.*https_logfmt_ssl_lst~ https_logfmt_ssl_lst/<NOSRV>.*31/000000001417C086/20/0 TLSv1.3/TLS_AES_256_GCM_SHA384"
 
     barrier b1 sync
 
     recv
-    expect ~ ".*https_logfmt_ssl_lst~ https_logfmt_ssl_lst/<NOSRV>.*34/00000000142090C1/0/0 TLSv1.3/\\(NONE\\)"
+    expect ~ "ERROR.*https_logfmt_ssl_lst~ https_logfmt_ssl_lst/<NOSRV>.*34/00000000142090C1/0/0 TLSv1.3/\\(NONE\\)"
 } -start
 
 syslog Slg_logconnerror -level info {
@@ -137,19 +137,19 @@
 
     listen cust_logfmt_ssl_lst
         log ${Slg_cust_fmt_addr}:${Slg_cust_fmt_port} local0
-	option log-error-via-logformat
         mode http
         log-format "conn_status:\"%[fc_conn_err]:%[fc_conn_err_str]\" hsk_err:\"%[ssl_fc_hsk_err]:%[ssl_fc_hsk_err_str]\" CN=%{+Q}[ssl_c_s_dn],serial=%[ssl_c_serial,hex],hash=%[ssl_c_sha1,hex]"
+        error-log-format "ERROR conn_status:\"%[fc_conn_err]:%[fc_conn_err_str]\" hsk_err:\"%[ssl_fc_hsk_err]:%[ssl_fc_hsk_err_str]\" CN=%{+Q}[ssl_c_s_dn],serial=%[ssl_c_serial,hex],hash=%[ssl_c_sha1,hex]"
         bind "${tmpdir}/cust_logfmt_ssl.sock" ssl crt ${testdir}/set_cafile_server.pem ca-verify-file ${testdir}/set_cafile_rootCA.crt ca-file ${testdir}/set_cafile_interCA1.crt verify required ciphersuites "TLS_AES_256_GCM_SHA384"
         server s1 ${s1_addr}:${s1_port}
 
     listen https_logfmt_ssl_lst
         log ${Slg_https_fmt_addr}:${Slg_https_fmt_port} local0 info
         log ${Slg_https_fmt_err_addr}:${Slg_https_fmt_err_port} local0 err info
-	option log-error-via-logformat
         option log-separate-errors
         mode http
         option httpslog
+        error-log-format "ERROR %ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r %[fc_conn_err]/%[ssl_fc_hsk_err,hex]/%[ssl_c_err]/%[ssl_c_ca_err] %sslv/%sslc"
         bind "${tmpdir}/https_logfmt_ssl.sock" ssl crt ${testdir}/set_cafile_server.pem ca-verify-file ${testdir}/set_cafile_rootCA.crt ca-file ${testdir}/set_cafile_interCA1.crt verify required ciphersuites "TLS_AES_256_GCM_SHA384"
         server s1 ${s1_addr}:${s1_port}
 
diff --git a/src/cfgparse-listen.c b/src/cfgparse-listen.c
index 790566a..d4a73e7 100644
--- a/src/cfgparse-listen.c
+++ b/src/cfgparse-listen.c
@@ -52,6 +52,7 @@
 	"fullconn", "dispatch", "balance", "hash-type",
 	"hash-balance-factor", "unique-id-format", "unique-id-header",
 	"log-format", "log-format-sd", "log-tag", "log", "source", "usesrc",
+	"error-log-format",
 	NULL /* must be last */
 };
 
@@ -2753,6 +2754,39 @@
 			err_code |= ERR_WARN;
 		}
 	}
+	else if (strcmp(args[0], "error-log-format") == 0) {
+		if (!*(args[1])) {
+			ha_alert("parsing [%s:%d] : %s expects an argument.\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		if (*(args[2])) {
+			ha_alert("parsing [%s:%d] : %s expects only one argument, don't forget to escape spaces!\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		if (curproxy->conf.error_logformat_string && curproxy->cap & PR_CAP_DEF) {
+			ha_warning("parsing [%s:%d]: 'error-log-format' overrides previous 'error-log-format' in 'defaults' section.\n",
+				   file, linenum);
+		}
+		free(curproxy->conf.error_logformat_string);
+		curproxy->conf.error_logformat_string = strdup(args[1]);
+		if (!curproxy->conf.error_logformat_string)
+			goto alloc_error;
+
+		free(curproxy->conf.elfs_file);
+		curproxy->conf.elfs_file = strdup(curproxy->conf.args.file);
+		curproxy->conf.elfs_line = curproxy->conf.args.line;
+
+		/* get a chance to improve log-format error reporting by
+		 * reporting the correct line-number when possible.
+		 */
+		if (!(curproxy->cap & PR_CAP_DEF) && !(curproxy->cap & PR_CAP_FE)) {
+			ha_warning("parsing [%s:%d] : backend '%s' : 'error-log-format' directive is ignored in backends.\n",
+				   file, linenum, curproxy->id);
+			err_code |= ERR_WARN;
+		}
+	}
 	else if (strcmp(args[0], "log-tag") == 0) {  /* tag to report to syslog */
 		if (*(args[1]) == 0) {
 			ha_alert("parsing [%s:%d] : '%s' expects a tag for use in syslog.\n", file, linenum, args[0]);
diff --git a/src/cfgparse.c b/src/cfgparse.c
index 2890fbf..9f6f3fe 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -3115,6 +3115,23 @@
 			curproxy->conf.args.line = 0;
 		}
 
+		if (curproxy->conf.error_logformat_string) {
+			curproxy->conf.args.ctx = ARGC_LOG;
+			curproxy->conf.args.file = curproxy->conf.elfs_file;
+			curproxy->conf.args.line = curproxy->conf.elfs_line;
+			err = NULL;
+			if (!parse_logformat_string(curproxy->conf.error_logformat_string, curproxy, &curproxy->logformat_error,
+			                            LOG_OPT_MANDATORY|LOG_OPT_MERGE_SPACES,
+			                            SMP_VAL_FE_LOG_END, &err)) {
+				ha_alert("Parsing [%s:%d]: failed to parse error-log-format : %s.\n",
+					 curproxy->conf.elfs_file, curproxy->conf.elfs_line, err);
+				free(err);
+				cfgerr++;
+			}
+			curproxy->conf.args.file = NULL;
+			curproxy->conf.args.line = 0;
+		}
+
 		/* only now we can check if some args remain unresolved.
 		 * This must be done after the users and groups resolution.
 		 */
diff --git a/src/log.c b/src/log.c
index 842e2c9..1804062 100644
--- a/src/log.c
+++ b/src/log.c
@@ -3126,7 +3126,10 @@
 		                             &sess->fe->logformat_sd);
 	}
 
-	size = sess_build_logline(sess, NULL, logline, global.max_syslog_len, &sess->fe->logformat);
+	if (!LIST_ISEMPTY(&sess->fe->logformat_error))
+		size = sess_build_logline(sess, NULL, logline, global.max_syslog_len, &sess->fe->logformat_error);
+	else
+		size = sess_build_logline(sess, NULL, logline, global.max_syslog_len, &sess->fe->logformat);
 	if (size > 0) {
 		_HA_ATOMIC_INC(&sess->fe->log_count);
 		__send_log(&sess->fe->logsrvs, &sess->fe->log_tag, level,
diff --git a/src/proxy.c b/src/proxy.c
index 16da542..e67ba39 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -178,6 +178,9 @@
 		free(p->conf.logformat_sd_string);
 	free(p->conf.lfsd_file);
 
+	free(p->conf.error_logformat_string);
+	free(p->conf.elfs_file);
+
 	list_for_each_entry_safe(cond, condb, &p->mon_fail_cond, list) {
 		LIST_DELETE(&cond->list);
 		prune_acl_cond(cond);
@@ -257,6 +260,13 @@
 		free(lf);
 	}
 
+	list_for_each_entry_safe(lf, lfb, &p->logformat_error, list) {
+		LIST_DELETE(&lf->list);
+		release_sample_expr(lf->expr);
+		free(lf->arg);
+		free(lf);
+	}
+
 	free_act_rules(&p->tcp_req.inspect_rules);
 	free_act_rules(&p->tcp_rep.inspect_rules);
 	free_act_rules(&p->tcp_req.l4_rules);
@@ -1321,6 +1331,7 @@
 	LIST_INIT(&p->logformat);
 	LIST_INIT(&p->logformat_sd);
 	LIST_INIT(&p->format_unique_id);
+	LIST_INIT(&p->logformat_error);
 	LIST_INIT(&p->conf.bind);
 	LIST_INIT(&p->conf.listeners);
 	LIST_INIT(&p->conf.errors);
@@ -1434,9 +1445,11 @@
 		ha_free(&defproxy->conf.logformat_sd_string);
 
 	ha_free(&defproxy->conf.uniqueid_format_string);
+	ha_free(&defproxy->conf.error_logformat_string);
 	ha_free(&defproxy->conf.lfs_file);
 	ha_free(&defproxy->conf.lfsd_file);
 	ha_free(&defproxy->conf.uif_file);
+	ha_free(&defproxy->conf.elfs_file);
 	chunk_destroy(&defproxy->log_tag);
 
 	free_email_alert(defproxy);
@@ -1668,6 +1681,15 @@
 			curproxy->conf.lfsd_file = strdup(defproxy->conf.lfsd_file);
 			curproxy->conf.lfsd_line = defproxy->conf.lfsd_line;
 		}
+
+		curproxy->conf.error_logformat_string = defproxy->conf.error_logformat_string;
+		if (curproxy->conf.error_logformat_string)
+			curproxy->conf.error_logformat_string = strdup(curproxy->conf.error_logformat_string);
+
+		if (defproxy->conf.elfs_file) {
+			curproxy->conf.elfs_file = strdup(defproxy->conf.elfs_file);
+			curproxy->conf.elfs_line = defproxy->conf.elfs_line;
+		}
 	}
 
 	if (curproxy->cap & PR_CAP_BE) {
diff --git a/src/session.c b/src/session.c
index 92d03ea..a11475e 100644
--- a/src/session.c
+++ b/src/session.c
@@ -357,8 +357,8 @@
 				conn->err_code = CO_ER_SSL_TIMEOUT;
 		}
 
-		if (sess->fe->options & PR_O_ERR_LOGFMT) {
-			/* Display a log line following the configured log-format. */
+		if(!LIST_ISEMPTY(&sess->fe->logformat_error)) {
+			/* Display a log line following the configured error-log-format. */
 			sess_log(sess);
 		}
 		else {