From 92bfda580cb8212114692fb0bf80e3ce15395342 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Mon, 1 Jun 2026 16:12:08 -0700 Subject: [PATCH] cmd/tailscale/cli: fix time in `tailscale routecheck` (#19956) When running `tailscale netcheck`, the reported timestamp used to be in UTC and formatted according to RFC 3339 with a `T` to separate the date from the time: sfllaw@h2co3:~$ tailscale netcheck | head -n3 Report: * Time: 2026-06-01T21:12:32.252620138Z This is machine-readable time leaking out to the user interface. Times in normal commands are formatted for humans to read: sfllaw@h2co3:~$ date Mon 01 Jun 2026 02:39:14 PM PDT sfllaw@h2co3:~$ journalctl -t tailscaled | tail -n1 Jun 01 14:35:21 h2co3 tailscaled[3328921]: wgengine: sending TSMP disco key advertisement to 100.90.144.102 sfllaw@h2co3:~$ timedatectl show Timezone=America/Los_Angeles LocalRTC=no CanNTP=yes NTP=yes NTPSynchronized=yes TimeUSec=Mon 2026-06-01 14:38:32 PDT RTCTimeUSec=Mon 2026-06-01 14:38:32 PDT sfllaw@h2co3:~$ uptime --since 2026-05-15 07:37:45 This PR makes the times printed by the CLI commands consistent: - For `tailscale routecheck`, it now prints local time as `2026-05-15 07:37:45-07:00`. - For `netlogfmt`, it has always printed local time with a space, but now includes the time zone. - All machine-readable outputs continue to be standard RFC 3339 in UTC, i.e. `--format=json`. As part of a general cleanup, this PR also adds standard common time.Format layouts as tstime constants. Fixes #19928 Signed-off-by: Simon Law --- cmd/netlogfmt/main.go | 3 ++- cmd/stund/depaware.txt | 1 + cmd/tailscale/cli/netcheck.go | 3 ++- ipn/localapi/localapi.go | 2 +- log/filelogger/log.go | 10 +++------- ssh/tailssh/tailssh.go | 5 +++-- tstime/format.go | 26 ++++++++++++++++++++++++++ tsweb/request_id.go | 3 ++- 8 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 tstime/format.go diff --git a/cmd/netlogfmt/main.go b/cmd/netlogfmt/main.go index 212b36fb6..af7baae46 100644 --- a/cmd/netlogfmt/main.go +++ b/cmd/netlogfmt/main.go @@ -45,6 +45,7 @@ import ( jsonv2 "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/bools" "tailscale.com/types/logid" "tailscale.com/types/netlogtype" @@ -294,7 +295,7 @@ func printMessage(msg message) { fmt.Printf("NodeID: %s\n", msg.NodeID) } formatTime := func(t time.Time) string { - return t.In(time.Local).Format("2006-01-02 15:04:05.000") + return t.Local().Format(tstime.DateSpTimeMilliZ) } switch { case !msg.Logged.IsZero(): diff --git a/cmd/stund/depaware.txt b/cmd/stund/depaware.txt index 7804915dc..239b728e1 100644 --- a/cmd/stund/depaware.txt +++ b/cmd/stund/depaware.txt @@ -61,6 +61,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar tailscale.com/net/tsaddr from tailscale.com/tsweb tailscale.com/syncs from tailscale.com/metrics+ tailscale.com/tailcfg from tailscale.com/version+ + tailscale.com/tstime from tailscale.com/tsweb tailscale.com/tsweb from tailscale.com/cmd/stund+ tailscale.com/tsweb/promvarz from tailscale.com/cmd/stund tailscale.com/tsweb/varz from tailscale.com/tsweb+ diff --git a/cmd/tailscale/cli/netcheck.go b/cmd/tailscale/cli/netcheck.go index c3f6f385d..52d66d516 100644 --- a/cmd/tailscale/cli/netcheck.go +++ b/cmd/tailscale/cli/netcheck.go @@ -26,6 +26,7 @@ import ( "tailscale.com/net/portmapper/portmappertype" "tailscale.com/net/tlsdial" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/logger" "tailscale.com/util/eventbus" "tailscale.com/util/set" @@ -175,7 +176,7 @@ func printNetCheckReport(dm *tailcfg.DERPMap, report *netcheck.Report) error { } printf("\nReport:\n") - printf("\t* Time: %v\n", report.Now.Format(time.RFC3339Nano)) + printf("\t* Time: %v\n", report.Now.Local().Format(tstime.DateSpTimeNanoZ)) printf("\t* UDP: %v\n", report.UDP) if report.GlobalV4.IsValid() { printf("\t* IPv4: yes, %s\n", report.GlobalV4) diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 6164daac0..8ce9acfd0 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -401,7 +401,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) { defer h.b.TryFlushLogs() // kick off upload after bugreport's done logging logMarker := func() string { - return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, h.clock.Now().UTC().Format("20060102150405Z"), rands.HexString(16)) + return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, h.clock.Now().UTC().Format(tstime.NumericDateTimeZ), rands.HexString(16)) } if envknob.NoLogsNoSupport() { logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" } diff --git a/log/filelogger/log.go b/log/filelogger/log.go index 268cf1bba..a9ce48555 100644 --- a/log/filelogger/log.go +++ b/log/filelogger/log.go @@ -16,6 +16,7 @@ import ( "sync" "time" + "tailscale.com/tstime" "tailscale.com/types/logger" ) @@ -106,13 +107,8 @@ func (w *logFileWriter) appendToFileLocked(out []byte) { } out = removeDatePrefix(out) if w.f != nil { - // RFC3339Nano but with a fixed number (3) of nanosecond digits: - const formatPre = "2006-01-02T15:04:05" - const formatPost = "Z07:00" - fmt.Fprintf(w.f, "%s.%03d%s: %s", - now.Format(formatPre), - now.Nanosecond()/int(time.Millisecond/time.Nanosecond), - now.Format(formatPost), + fmt.Fprintf(w.f, "%s: %s", + now.Format(tstime.DateTTimeMilliZ), out) } } diff --git a/ssh/tailssh/tailssh.go b/ssh/tailssh/tailssh.go index e01f78eb3..49dc0daf1 100644 --- a/ssh/tailssh/tailssh.go +++ b/ssh/tailssh/tailssh.go @@ -39,6 +39,7 @@ import ( "tailscale.com/net/tsdial" "tailscale.com/sessionrecording" "tailscale.com/tailcfg" + "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/types/netmap" @@ -479,7 +480,7 @@ func (srv *server) newConn() (*conn, error) { srv.mu.Unlock() c := &conn{srv: srv} now := srv.now() - c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5)) + c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format(tstime.BasicDateTTime), randBytes(5)) fwdHandler := &gliderssh.ForwardedTCPHandler{} streamLocalFwdHandler := &gliderssh.ForwardedUnixHandler{} c.Server = &gliderssh.Server{ @@ -776,7 +777,7 @@ func (ss *sshSession) vlogf(format string, args ...any) { } func (c *conn) newSSHSession(s gliderssh.Session) *sshSession { - sharedID := fmt.Sprintf("sess-%s-%02x", c.srv.now().UTC().Format("20060102T150405"), randBytes(5)) + sharedID := fmt.Sprintf("sess-%s-%02x", c.srv.now().UTC().Format(tstime.BasicDateTTime), randBytes(5)) c.logf("starting session: %v", sharedID) ctx, cancel := context.WithCancelCause(s.Context()) return &sshSession{ diff --git a/tstime/format.go b/tstime/format.go new file mode 100644 index 000000000..dd1a956d7 --- /dev/null +++ b/tstime/format.go @@ -0,0 +1,26 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package tstime + +import "time" + +// These are additional layouts for use in [time.Format] and [time.Parse]. +// For additional details, see [time.Layout]. +const ( + // time.RFC3339 = "2006-01-02T15:04:05Z07:00" + DateTTimeMilliZ = "2006-01-02T15:04:05.000Z07:00" // RFC3339 with fixed milliseconds + + // time.DateTime = "2006-01-02 15:04:05" + DateSpTimeZ = time.DateTime + "Z07:00" // RFC3339 with space instead of 'T' + DateSpTimeMilliZ = time.DateTime + ".999Z07:00" + DateSpTimeNanoZ = time.DateTime + ".999999999Z07:00" + + // ISO8601 basic format: without punctuation + BasicDateTTime = "20060102T150405" + BasicDateTTimeZ = "20060102T150405Z07:00" + + // ISO8601 numeric format: removed in 2019 + NumericDateTime = "20060102150405" + NumericDateTimeZ = "20060102150405Z07:00" +) diff --git a/tsweb/request_id.go b/tsweb/request_id.go index 351ed1710..c32c3d9bb 100644 --- a/tsweb/request_id.go +++ b/tsweb/request_id.go @@ -8,6 +8,7 @@ import ( "net/http" "time" + "tailscale.com/tstime" "tailscale.com/util/ctxkey" "tailscale.com/util/rands" ) @@ -47,7 +48,7 @@ func GenerateRequestID() RequestID { // Now we return "REQ-2" version, where the "2" doubles as the year 2YYY // in a leading date. now := time.Now().UTC() - return RequestID("REQ-" + now.Format("20060102150405") + rands.HexString(16)) + return RequestID("REQ-" + now.Format(tstime.NumericDateTime) + rands.HexString(16)) } // SetRequestID is an HTTP middleware that injects a RequestID in the