mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-06-23 04:10:20 +00:00
TUN-10391: Avoid using fmt.Println
Avoid using fmt.Println and instead switch to logging pre-checks with the provided logger.
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
package cliutil
|
package cliutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
|
||||||
@@ -57,3 +60,57 @@ func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
|
|||||||
FlagLogOutput,
|
FlagLogOutput,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogTable renders lines inside an ASCII table and logs each rendered row.
|
||||||
|
func LogTable(log *zerolog.Logger, lines []string, title ...string) {
|
||||||
|
tableTitle := ""
|
||||||
|
if len(title) > 0 {
|
||||||
|
tableTitle = title[0]
|
||||||
|
}
|
||||||
|
for _, line := range asciiBox(lines, tableTitle, 2) {
|
||||||
|
if line != "" {
|
||||||
|
log.Info().Msg(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// asciiBox wraps lines in a bordered ASCII box with an optional title row.
|
||||||
|
func asciiBox(lines []string, title string, padding int) (box []string) {
|
||||||
|
maxLen := maxLen(lines, title)
|
||||||
|
spacer := strings.Repeat(" ", padding)
|
||||||
|
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||||
|
box = append(box, border)
|
||||||
|
if title != "" {
|
||||||
|
box = append(box, renderBoxLine(centerLine(title, maxLen), maxLen, spacer))
|
||||||
|
box = append(box, border)
|
||||||
|
}
|
||||||
|
for _, line := range lines {
|
||||||
|
box = append(box, renderBoxLine(line, maxLen, spacer))
|
||||||
|
}
|
||||||
|
box = append(box, border)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderBoxLine pads a single line so it fills the box width.
|
||||||
|
func renderBoxLine(line string, maxLen int, spacer string) string {
|
||||||
|
return "|" + spacer + line + strings.Repeat(" ", maxLen-len(line)) + spacer + "|"
|
||||||
|
}
|
||||||
|
|
||||||
|
// centerLine pads line evenly so it is centered within width.
|
||||||
|
func centerLine(line string, width int) string {
|
||||||
|
padding := width - len(line)
|
||||||
|
leftPadding := padding / 2
|
||||||
|
rightPadding := padding - leftPadding
|
||||||
|
return strings.Repeat(" ", leftPadding) + line + strings.Repeat(" ", rightPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxLen returns the longest visible line length including the title.
|
||||||
|
func maxLen(lines []string, title string) int {
|
||||||
|
max := len(title)
|
||||||
|
for _, line := range lines {
|
||||||
|
if len(line) > max {
|
||||||
|
max = len(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package cliutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogTableWithoutTitle(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
lines := captureTableLogs(t, []string{"first", "second"})
|
||||||
|
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"+----------+",
|
||||||
|
"| first |",
|
||||||
|
"| second |",
|
||||||
|
"+----------+",
|
||||||
|
}, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogTableWithTitle(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
lines := captureTableLogs(t, []string{"first", "second"}, "TT")
|
||||||
|
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"+----------+",
|
||||||
|
"| TT |",
|
||||||
|
"+----------+",
|
||||||
|
"| first |",
|
||||||
|
"| second |",
|
||||||
|
"+----------+",
|
||||||
|
}, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func captureTableLogs(t *testing.T, lines []string, title ...string) []string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := zerolog.New(&buf)
|
||||||
|
|
||||||
|
LogTable(&logger, lines, title...)
|
||||||
|
|
||||||
|
// nolint: prealloc
|
||||||
|
var messages []string
|
||||||
|
for _, line := range bytes.Split(bytes.TrimSpace(buf.Bytes()), []byte("\n")) {
|
||||||
|
var entry struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
require.NoError(t, json.Unmarshal(line, &entry))
|
||||||
|
messages = append(messages, entry.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages
|
||||||
|
}
|
||||||
@@ -551,8 +551,8 @@ func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
|
|||||||
|
|
||||||
report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers)
|
report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers)
|
||||||
|
|
||||||
// Output the human-readable table to console
|
// Output the human-readable table
|
||||||
fmt.Println(report.String())
|
cliutil.LogTable(log, report.String(), "CONNECTIVITY PRE-CHECKS")
|
||||||
|
|
||||||
// Also log structured results for log aggregation
|
// Also log structured results for log aggregation
|
||||||
report.LogEvent(log)
|
report.LogEvent(log)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
||||||
"github.com/cloudflare/cloudflared/connection"
|
"github.com/cloudflare/cloudflared/connection"
|
||||||
)
|
)
|
||||||
@@ -44,7 +45,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to request quick Tunnel")
|
return errors.Wrap(err, "failed to request quick Tunnel")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
// This will read the entire response into memory so we can print it in case of error
|
// This will read the entire response into memory so we can print it in case of error
|
||||||
rsp_body, err := io.ReadAll(resp.Body)
|
rsp_body, err := io.ReadAll(resp.Body)
|
||||||
@@ -76,12 +77,10 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
|||||||
url = "https://" + url
|
url = "https://" + url
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, line := range AsciiBox([]string{
|
cliutil.LogTable(sc.log, []string{
|
||||||
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
|
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
|
||||||
url,
|
url,
|
||||||
}, 2) {
|
})
|
||||||
sc.log.Info().Msg(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !sc.c.IsSet(flags.Protocol) {
|
if !sc.c.IsSet(flags.Protocol) {
|
||||||
_ = sc.c.Set(flags.Protocol, "quic")
|
_ = sc.c.Set(flags.Protocol, "quic")
|
||||||
@@ -116,26 +115,3 @@ type QuickTunnel struct {
|
|||||||
AccountTag string `json:"account_tag"`
|
AccountTag string `json:"account_tag"`
|
||||||
Secret []byte `json:"secret"`
|
Secret []byte `json:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print out the given lines in a nice ASCII box.
|
|
||||||
func AsciiBox(lines []string, padding int) (box []string) {
|
|
||||||
maxLen := maxLen(lines)
|
|
||||||
spacer := strings.Repeat(" ", padding)
|
|
||||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
|
||||||
box = append(box, border)
|
|
||||||
for _, line := range lines {
|
|
||||||
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
|
|
||||||
}
|
|
||||||
box = append(box, border)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func maxLen(lines []string) int {
|
|
||||||
max := 0
|
|
||||||
for _, line := range lines {
|
|
||||||
if len(line) > max {
|
|
||||||
max = len(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ Integration tests for cloudflared connectivity pre-checks (TUN-10391).
|
|||||||
Scope
|
Scope
|
||||||
-----
|
-----
|
||||||
These tests verify the end-to-end behavior of cloudflared pre-checks:
|
These tests verify the end-to-end behavior of cloudflared pre-checks:
|
||||||
- that the human-readable table written to stdout has the correct structure
|
- that the human-readable table written to the log output has the correct
|
||||||
and content,
|
structure and content,
|
||||||
- that structured JSON log lines are emitted with the expected fields, and
|
- that structured JSON log lines are emitted with the expected fields, and
|
||||||
- that running the `diag` subcommand against a live tunnel instance produces a
|
- that running the `diag` subcommand against a live tunnel instance produces a
|
||||||
zip archive that contains prechecks.json.
|
zip archive that contains prechecks.json.
|
||||||
@@ -25,15 +25,24 @@ without real firewall intervention is:
|
|||||||
DNS failure and Management API failure cannot be triggered via CLI flags alone;
|
DNS failure and Management API failure cannot be triggered via CLI flags alone;
|
||||||
they require network-level intervention outside the component-test harness.
|
they require network-level intervention outside the component-test harness.
|
||||||
|
|
||||||
stdout design
|
stdout/stderr design
|
||||||
-------------
|
--------------------
|
||||||
fmt.Println(report.String()) runs inside a goroutine that is started
|
The pre-checks table is emitted via cliutil.LogTable, which wraps the content
|
||||||
concurrently with the tunnel. We poll a --logfile for the "precheck complete"
|
in an ASCII box and logs each line at Info level through zerolog. zerolog
|
||||||
sentinel before leaving the `with` block, ensuring the goroutine has finished.
|
writes to stderr, which the test harness merges into stdout (stderr=STDOUT in
|
||||||
We then call cfd.terminate(). After the `with` block exits, the process is
|
Popen). We poll a --logfile for the "precheck complete" sentinel before
|
||||||
dead and all output has been captured by CloudflaredProcess's background reader
|
leaving the `with` block, ensuring the goroutine has finished. We then call
|
||||||
thread (stderr is merged into stdout). We read the accumulated lines from
|
cfd.terminate(). After the `with` block exits, the process is dead and all
|
||||||
cfd.stdout_lines.
|
output has been captured by CloudflaredProcess's background reader thread. We
|
||||||
|
read the accumulated lines from cfd.stdout_lines.
|
||||||
|
|
||||||
|
Box format (cliutil.asciiBox with padding=2, title="CONNECTIVITY PRE-CHECKS"):
|
||||||
|
+----...----+
|
||||||
|
| CONNECTIVITY PRE-CHECKS | (centered title)
|
||||||
|
+----...----+
|
||||||
|
| COMPONENT TARGET ... | (content rows)
|
||||||
|
...
|
||||||
|
+----...----+
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -46,11 +55,13 @@ import zipfile as zipfilemod
|
|||||||
from constants import METRICS_PORT
|
from constants import METRICS_PORT
|
||||||
from util import LOGGER, start_cloudflared, wait_tunnel_ready
|
from util import LOGGER, start_cloudflared, wait_tunnel_ready
|
||||||
|
|
||||||
# stdout table constants
|
# ASCII box constants (cliutil.asciiBox, padding=2, title="CONNECTIVITY PRE-CHECKS")
|
||||||
TABLE_WIDTH = 80
|
BOX_TITLE = "CONNECTIVITY PRE-CHECKS"
|
||||||
HEADER_LINE = "--- CONNECTIVITY PRE-CHECKS " + "-" * (TABLE_WIDTH - len("--- CONNECTIVITY PRE-CHECKS ") - 1)
|
BOX_BORDER_RE = re.compile(r"^\+(-+)\+$", re.MULTILINE) # matches +----...----+
|
||||||
COL_HEADER = "COMPONENT" # first token of the column-header row
|
COL_HEADER = "COMPONENT" # first word of the column-header row
|
||||||
SEPARATOR = "-" * TABLE_WIDTH
|
|
||||||
|
# zerolog console format: "2006-01-02T15:04:05Z LVL <message>"
|
||||||
|
_LOG_PREFIX_RE = re.compile(r"^\S+ \w+ ")
|
||||||
|
|
||||||
# Component names (probes.go: componentXxx)
|
# Component names (probes.go: componentXxx)
|
||||||
COMP_DNS = "DNS Resolution"
|
COMP_DNS = "DNS Resolution"
|
||||||
@@ -164,23 +175,46 @@ class TableRow:
|
|||||||
return f"TableRow({self.component!r}, {self.target!r}, {self.status!r}, {self.details!r})"
|
return f"TableRow({self.component!r}, {self.target!r}, {self.status!r}, {self.details!r})"
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_log_prefix(line: str) -> str:
|
||||||
|
"""Remove the zerolog console prefix ('2006-01-02T15:04:05Z LVL ') if present."""
|
||||||
|
return _LOG_PREFIX_RE.sub("", line, count=1)
|
||||||
|
|
||||||
|
|
||||||
|
def _unbox_line(line: str) -> str:
|
||||||
|
"""Strip the box border padding from a content line: '| text |' -> 'text'.
|
||||||
|
|
||||||
|
Accepts lines that may still carry a zerolog console prefix; the prefix is
|
||||||
|
removed before the box delimiters are stripped.
|
||||||
|
"""
|
||||||
|
msg = _strip_log_prefix(line)
|
||||||
|
if msg.startswith("|") and msg.endswith("|"):
|
||||||
|
return msg[1:-1].strip()
|
||||||
|
return msg.strip()
|
||||||
|
|
||||||
|
|
||||||
def _parse_table(stdout: str) -> list[TableRow]:
|
def _parse_table(stdout: str) -> list[TableRow]:
|
||||||
"""
|
"""
|
||||||
Parse the data rows from a precheck table in stdout.
|
Parse the data rows from a precheck table in stdout.
|
||||||
|
|
||||||
text/tabwriter uses padding=2, so columns are separated by two or more
|
The table is now wrapped in an ASCII box by cliutil.LogTable. Each
|
||||||
spaces. We skip the column-header row and stop at blank lines, SUMMARY,
|
content line has the form '| <content> |', optionally preceded by a
|
||||||
separator, ERROR, or WARNING lines.
|
zerolog console prefix. We strip both the prefix and the box borders
|
||||||
|
before splitting on two-or-more spaces (text/tabwriter padding=2).
|
||||||
|
|
||||||
|
We skip the column-header row and stop at blank lines, SUMMARY, box
|
||||||
|
border lines, ERROR, or WARNING lines.
|
||||||
"""
|
"""
|
||||||
rows = []
|
rows = []
|
||||||
in_data = False
|
in_data = False
|
||||||
for line in stdout.splitlines():
|
for raw_line in stdout.splitlines():
|
||||||
|
msg = _strip_log_prefix(raw_line)
|
||||||
|
line = _unbox_line(raw_line)
|
||||||
if line.startswith("COMPONENT"):
|
if line.startswith("COMPONENT"):
|
||||||
in_data = True
|
in_data = True
|
||||||
continue
|
continue
|
||||||
if not in_data:
|
if not in_data:
|
||||||
continue
|
continue
|
||||||
if (line == "" or line.startswith("SUMMARY") or line.startswith("---")
|
if (line == "" or line.startswith("SUMMARY") or BOX_BORDER_RE.match(msg)
|
||||||
or line.startswith("ERROR") or line.startswith("WARNING")):
|
or line.startswith("ERROR") or line.startswith("WARNING")):
|
||||||
in_data = False
|
in_data = False
|
||||||
continue
|
continue
|
||||||
@@ -261,14 +295,18 @@ class TestPrechecksHappyPath:
|
|||||||
LOGGER.debug(f"[happy-path] stdout:\n{stdout}")
|
LOGGER.debug(f"[happy-path] stdout:\n{stdout}")
|
||||||
LOGGER.debug(f"[happy-path] log_lines:\n{log_lines}")
|
LOGGER.debug(f"[happy-path] log_lines:\n{log_lines}")
|
||||||
|
|
||||||
|
# Strip zerolog console prefixes so pattern matching works on raw messages.
|
||||||
|
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
|
||||||
|
|
||||||
# ── table structure ──────────────────────────────────────────────────
|
# ── table structure ──────────────────────────────────────────────────
|
||||||
# stderr is merged into stdout so log lines precede the table.
|
# zerolog writes to stderr which is merged into stdout by the harness.
|
||||||
assert HEADER_LINE in stdout, \
|
# The table is wrapped in an ASCII box by cliutil.LogTable.
|
||||||
f"Expected header line in output;\ngot:\n{stdout}"
|
assert BOX_TITLE in messages, \
|
||||||
assert COL_HEADER in stdout, \
|
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
|
||||||
|
assert COL_HEADER in messages, \
|
||||||
f"Expected column header row in output;\ngot:\n{stdout}"
|
f"Expected column header row in output;\ngot:\n{stdout}"
|
||||||
assert SEPARATOR in stdout, \
|
assert BOX_BORDER_RE.search(messages), \
|
||||||
f"Expected closing separator in output;\ngot:\n{stdout}"
|
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
|
||||||
|
|
||||||
# ── row content ──────────────────────────────────────────────────────
|
# ── row content ──────────────────────────────────────────────────────
|
||||||
rows = _parse_table(stdout)
|
rows = _parse_table(stdout)
|
||||||
@@ -306,11 +344,11 @@ class TestPrechecksHappyPath:
|
|||||||
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
|
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
|
||||||
|
|
||||||
# ── no action lines ──────────────────────────────────────────────────
|
# ── no action lines ──────────────────────────────────────────────────
|
||||||
assert PREFIX_ERROR not in stdout, f"Unexpected ERROR action:\n{stdout}"
|
assert PREFIX_ERROR not in messages, f"Unexpected ERROR action:\n{stdout}"
|
||||||
assert PREFIX_WARNING not in stdout, f"Unexpected WARNING action:\n{stdout}"
|
assert PREFIX_WARNING not in messages, f"Unexpected WARNING action:\n{stdout}"
|
||||||
|
|
||||||
# ── exact summary line ───────────────────────────────────────────────
|
# ── summary line ─────────────────────────────────────────────────────
|
||||||
assert SUMMARY_HEALTHY in stdout, \
|
assert SUMMARY_HEALTHY in messages, \
|
||||||
f"Expected healthy summary;\ngot:\n{stdout}"
|
f"Expected healthy summary;\ngot:\n{stdout}"
|
||||||
|
|
||||||
# ── structured log ───────────────────────────────────────────────────
|
# ── structured log ───────────────────────────────────────────────────
|
||||||
@@ -366,14 +404,18 @@ class TestPrechecksHardFail:
|
|||||||
LOGGER.debug(f"[hard-fail] stdout:\n{stdout}")
|
LOGGER.debug(f"[hard-fail] stdout:\n{stdout}")
|
||||||
LOGGER.debug(f"[hard-fail] log_lines:\n{log_lines}")
|
LOGGER.debug(f"[hard-fail] log_lines:\n{log_lines}")
|
||||||
|
|
||||||
|
# Strip zerolog console prefixes so pattern matching works on raw messages.
|
||||||
|
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
|
||||||
|
|
||||||
# ── table structure ──────────────────────────────────────────────────
|
# ── table structure ──────────────────────────────────────────────────
|
||||||
# stderr is merged into stdout so log lines precede the table.
|
# zerolog writes to stderr which is merged into stdout by the harness.
|
||||||
assert HEADER_LINE in stdout, \
|
# The table is wrapped in an ASCII box by cliutil.LogTable.
|
||||||
f"Expected header line in output;\ngot:\n{stdout}"
|
assert BOX_TITLE in messages, \
|
||||||
assert COL_HEADER in stdout, \
|
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
|
||||||
|
assert COL_HEADER in messages, \
|
||||||
f"Expected column header row in output;\ngot:\n{stdout}"
|
f"Expected column header row in output;\ngot:\n{stdout}"
|
||||||
assert SEPARATOR in stdout, \
|
assert BOX_BORDER_RE.search(messages), \
|
||||||
f"Expected closing separator in output;\ngot:\n{stdout}"
|
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
|
||||||
|
|
||||||
# ── row content ──────────────────────────────────────────────────────
|
# ── row content ──────────────────────────────────────────────────────
|
||||||
rows = _parse_table(stdout)
|
rows = _parse_table(stdout)
|
||||||
@@ -404,12 +446,12 @@ class TestPrechecksHardFail:
|
|||||||
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
|
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
|
||||||
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
|
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
|
||||||
|
|
||||||
assert f"{PREFIX_ERROR}{ACTION_QUIC_BLOCKED}" in stdout, \
|
assert f"{PREFIX_ERROR}{ACTION_QUIC_BLOCKED}" in messages, \
|
||||||
f"Expected QUIC ERROR action;\ngot:\n{stdout}"
|
f"Expected QUIC ERROR action;\ngot:\n{stdout}"
|
||||||
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in stdout, \
|
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in messages, \
|
||||||
f"Expected HTTP/2 ERROR action;\ngot:\n{stdout}"
|
f"Expected HTTP/2 ERROR action;\ngot:\n{stdout}"
|
||||||
|
|
||||||
assert SUMMARY_CRITICAL in stdout, \
|
assert SUMMARY_CRITICAL in messages, \
|
||||||
f"Expected critical summary;\ngot:\n{stdout}"
|
f"Expected critical summary;\ngot:\n{stdout}"
|
||||||
|
|
||||||
_assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None)
|
_assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None)
|
||||||
|
|||||||
+14
-51
@@ -10,19 +10,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// tableWidth is the total character width of the separator lines.
|
|
||||||
tableWidth = 80
|
|
||||||
|
|
||||||
// Status names.
|
// Status names.
|
||||||
passStatus = "PASS"
|
passStatus = "PASS"
|
||||||
failStatus = "FAIL"
|
failStatus = "FAIL"
|
||||||
skipStatus = "SKIP"
|
skipStatus = "SKIP"
|
||||||
unknownStatus = "UNKNOWN"
|
unknownStatus = "UNKNOWN"
|
||||||
|
|
||||||
// Section separators.
|
|
||||||
sectionChar = "-"
|
|
||||||
headerTitle = "CONNECTIVITY PRE-CHECKS"
|
|
||||||
|
|
||||||
// Log message constants.
|
// Log message constants.
|
||||||
logMsgPrecheck = "precheck"
|
logMsgPrecheck = "precheck"
|
||||||
logMsgPrecheckComplete = "precheck complete"
|
logMsgPrecheckComplete = "precheck complete"
|
||||||
@@ -35,8 +28,6 @@ const (
|
|||||||
logFieldDetails = "details"
|
logFieldDetails = "details"
|
||||||
logFieldHardFail = "hard_fail"
|
logFieldHardFail = "hard_fail"
|
||||||
logFieldSuggestedProtocol = "suggested_protocol"
|
logFieldSuggestedProtocol = "suggested_protocol"
|
||||||
|
|
||||||
sep = " "
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// statusLabel returns the display label for a given Status.
|
// statusLabel returns the display label for a given Status.
|
||||||
@@ -58,21 +49,9 @@ func (s Status) logString() string {
|
|||||||
return strings.ToLower(s.String())
|
return strings.ToLower(s.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// separator returns a full-width horizontal line.
|
|
||||||
func separator() string {
|
|
||||||
return strings.Repeat(sectionChar, tableWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// header returns the top section title line.
|
|
||||||
func header() string {
|
|
||||||
leftDashes := strings.Repeat(sectionChar, 3)
|
|
||||||
rightLen := tableWidth - len(leftDashes) - len(headerTitle) - len(sep)*2
|
|
||||||
return leftDashes + sep + headerTitle + sep + strings.Repeat(sectionChar, rightLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderTable uses text/tabwriter to format the results rows with
|
// renderTable uses text/tabwriter to format the results rows with
|
||||||
// automatically aligned columns, returning the rendered string.
|
// automatically aligned columns, returning the rendered lines.
|
||||||
func renderTable(results []CheckResult) string {
|
func renderTable(results []CheckResult) []string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
// minwidth=0, tabwidth=8, padding=2, padchar=' ', flags=0
|
// minwidth=0, tabwidth=8, padding=2, padchar=' ', flags=0
|
||||||
w := tabwriter.NewWriter(&buf, 0, 8, 2, ' ', 0)
|
w := tabwriter.NewWriter(&buf, 0, 8, 2, ' ', 0)
|
||||||
@@ -81,27 +60,27 @@ func renderTable(results []CheckResult) string {
|
|||||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Component, r.Target, r.ProbeStatus.statusLabel(), r.Details)
|
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Component, r.Target, r.ProbeStatus.statusLabel(), r.Details)
|
||||||
}
|
}
|
||||||
_ = w.Flush()
|
_ = w.Flush()
|
||||||
return buf.String()
|
return strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderActions collects all non-empty Action strings from results and returns
|
// renderActions collects all non-empty Action strings from results and returns
|
||||||
// the formatted warning/error block that appears between the table and SUMMARY.
|
// the formatted warning/error block that appears between the table and SUMMARY.
|
||||||
// A Fail result is rendered as ERROR when the report is a hard fail, and as
|
// A Fail result is rendered as ERROR when the report is a hard fail, and as
|
||||||
// WARNING otherwise (degraded but tunnel can still run).
|
// WARNING otherwise (degraded but tunnel can still run).
|
||||||
func renderActions(r Report) string {
|
func renderActions(r Report) []string {
|
||||||
hardFail := r.hasHardFail()
|
hardFail := r.hasHardFail()
|
||||||
var sb strings.Builder
|
actions := make([]string, 0)
|
||||||
for _, res := range r.Results {
|
for _, res := range r.Results {
|
||||||
if res.Action == "" || res.ProbeStatus != Fail {
|
if res.Action == "" || res.ProbeStatus != Fail {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if hardFail {
|
if hardFail {
|
||||||
_, _ = fmt.Fprintf(&sb, "ERROR: %s\n", res.Action)
|
actions = append(actions, fmt.Sprintf("ERROR: %s", res.Action))
|
||||||
} else {
|
} else {
|
||||||
_, _ = fmt.Fprintf(&sb, "WARNING: %s\n", res.Action)
|
actions = append(actions, fmt.Sprintf("WARNING: %s", res.Action))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.String()
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
// summaryLine builds the SUMMARY: line based on the Report state.
|
// summaryLine builds the SUMMARY: line based on the Report state.
|
||||||
@@ -181,28 +160,12 @@ func (r Report) hasWarn() bool {
|
|||||||
return (quicFail != http2Fail) || apiFail
|
return (quicFail != http2Fail) || apiFail
|
||||||
}
|
}
|
||||||
|
|
||||||
// String renders the Report as a human-readable table suitable for os.Stdout.
|
// String renders the Report as human-readable table lines suitable for logging.
|
||||||
func (r Report) String() string {
|
func (r Report) String() []string {
|
||||||
var sb strings.Builder
|
lines := renderTable(r.Results)
|
||||||
|
lines = append(lines, renderActions(r)...)
|
||||||
sb.WriteString(header())
|
lines = append(lines, "", summaryLine(r))
|
||||||
sb.WriteString("\n")
|
return lines
|
||||||
|
|
||||||
sb.WriteString(renderTable(r.Results))
|
|
||||||
|
|
||||||
actions := renderActions(r)
|
|
||||||
if actions != "" {
|
|
||||||
sb.WriteString(actions)
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString("\n")
|
|
||||||
sb.WriteString(summaryLine(r))
|
|
||||||
sb.WriteString("\n")
|
|
||||||
|
|
||||||
sb.WriteString(separator())
|
|
||||||
sb.WriteString("\n")
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogEvent emits each CheckResult as a structured zerolog log line, followed by
|
// LogEvent emits each CheckResult as a structured zerolog log line, followed by
|
||||||
|
|||||||
+57
-62
@@ -134,85 +134,80 @@ func dnsFailReport() Report {
|
|||||||
|
|
||||||
func TestString_AllPass(t *testing.T) {
|
func TestString_AllPass(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
want := "" +
|
want := []string{
|
||||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
"COMPONENT TARGET STATUS DETAILS",
|
||||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||||
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||||
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
|
||||||
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful\n" +
|
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
|
||||||
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
|
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
|
||||||
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
|
"",
|
||||||
"\n" +
|
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.",
|
||||||
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.\n" +
|
}
|
||||||
"--------------------------------------------------------------------------------\n"
|
|
||||||
assert.Equal(t, want, allPassReport().String())
|
assert.Equal(t, want, allPassReport().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestString_QuicBlocked(t *testing.T) {
|
func TestString_QuicBlocked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
want := "" +
|
want := []string{
|
||||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
"COMPONENT TARGET STATUS DETAILS",
|
||||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||||
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||||
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
|
||||||
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed\n" +
|
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
|
||||||
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
|
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
|
||||||
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
|
"WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.",
|
||||||
"WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.\n" +
|
"",
|
||||||
"\n" +
|
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.",
|
||||||
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.\n" +
|
}
|
||||||
"--------------------------------------------------------------------------------\n"
|
|
||||||
assert.Equal(t, want, quicBlockedReport().String())
|
assert.Equal(t, want, quicBlockedReport().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestString_APIFail(t *testing.T) {
|
func TestString_APIFail(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
want := "" +
|
want := []string{
|
||||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
"COMPONENT TARGET STATUS DETAILS",
|
||||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||||
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||||
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
|
||||||
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful\n" +
|
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
|
||||||
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
|
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
|
||||||
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\n" +
|
"WARNING: cloudflared will still run, but automatic software updates are unavailable. Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates.",
|
||||||
"WARNING: cloudflared will still run, but automatic software updates are unavailable. Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates.\n" +
|
"",
|
||||||
"\n" +
|
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.",
|
||||||
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.\n" +
|
}
|
||||||
"--------------------------------------------------------------------------------\n"
|
|
||||||
assert.Equal(t, want, apiFailReport().String())
|
assert.Equal(t, want, apiFailReport().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestString_BothTransportsBlocked(t *testing.T) {
|
func TestString_BothTransportsBlocked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
want := "" +
|
want := []string{
|
||||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
"COMPONENT TARGET STATUS DETAILS",
|
||||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||||
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||||
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
|
||||||
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed\n" +
|
"TCP Connectivity Port 7844 (HTTP/2) FAIL HTTP/2 connection is blocked or unreachable",
|
||||||
"TCP Connectivity Port 7844 (HTTP/2) FAIL HTTP/2 connection is blocked or unreachable\n" +
|
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
|
||||||
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
|
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
|
||||||
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.\n" +
|
"",
|
||||||
"\n" +
|
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.",
|
||||||
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" +
|
}
|
||||||
"--------------------------------------------------------------------------------\n"
|
|
||||||
assert.Equal(t, want, bothTransportsBlockedReport().String())
|
assert.Equal(t, want, bothTransportsBlockedReport().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestString_DNSFail(t *testing.T) {
|
func TestString_DNSFail(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
want := "" +
|
want := []string{
|
||||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
"COMPONENT TARGET STATUS DETAILS",
|
||||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned",
|
||||||
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned\n" +
|
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned",
|
||||||
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned\n" +
|
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed",
|
||||||
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed\n" +
|
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed",
|
||||||
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed\n" +
|
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
|
||||||
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\n" +
|
"ERROR: Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.",
|
||||||
"ERROR: Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.\n" +
|
"",
|
||||||
"\n" +
|
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.",
|
||||||
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" +
|
}
|
||||||
"--------------------------------------------------------------------------------\n"
|
|
||||||
assert.Equal(t, want, dnsFailReport().String())
|
assert.Equal(t, want, dnsFailReport().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,9 +216,9 @@ func TestString_EmptyResults(t *testing.T) {
|
|||||||
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.QUIC)}
|
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.QUIC)}
|
||||||
out := r.String()
|
out := r.String()
|
||||||
// Must not panic and must still emit a valid skeleton.
|
// Must not panic and must still emit a valid skeleton.
|
||||||
assert.Contains(t, out, "CONNECTIVITY PRE-CHECKS")
|
require.Len(t, out, 3)
|
||||||
assert.Contains(t, out, "SUMMARY:")
|
assert.Contains(t, out[0], "COMPONENT")
|
||||||
assert.Contains(t, out, separator())
|
assert.Contains(t, out[2], "SUMMARY:")
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogEvent() / structured log renderer tests
|
// LogEvent() / structured log renderer tests
|
||||||
|
|||||||
Reference in New Issue
Block a user