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 <sfllaw@tailscale.com>
This commit is contained in:
@@ -45,6 +45,7 @@ import (
|
|||||||
jsonv2 "github.com/go-json-experiment/json"
|
jsonv2 "github.com/go-json-experiment/json"
|
||||||
"github.com/go-json-experiment/json/jsontext"
|
"github.com/go-json-experiment/json/jsontext"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/tstime"
|
||||||
"tailscale.com/types/bools"
|
"tailscale.com/types/bools"
|
||||||
"tailscale.com/types/logid"
|
"tailscale.com/types/logid"
|
||||||
"tailscale.com/types/netlogtype"
|
"tailscale.com/types/netlogtype"
|
||||||
@@ -294,7 +295,7 @@ func printMessage(msg message) {
|
|||||||
fmt.Printf("NodeID: %s\n", msg.NodeID)
|
fmt.Printf("NodeID: %s\n", msg.NodeID)
|
||||||
}
|
}
|
||||||
formatTime := func(t time.Time) string {
|
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 {
|
switch {
|
||||||
case !msg.Logged.IsZero():
|
case !msg.Logged.IsZero():
|
||||||
|
|||||||
@@ -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/net/tsaddr from tailscale.com/tsweb
|
||||||
tailscale.com/syncs from tailscale.com/metrics+
|
tailscale.com/syncs from tailscale.com/metrics+
|
||||||
tailscale.com/tailcfg from tailscale.com/version+
|
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 from tailscale.com/cmd/stund+
|
||||||
tailscale.com/tsweb/promvarz from tailscale.com/cmd/stund
|
tailscale.com/tsweb/promvarz from tailscale.com/cmd/stund
|
||||||
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
"tailscale.com/net/portmapper/portmappertype"
|
"tailscale.com/net/portmapper/portmappertype"
|
||||||
"tailscale.com/net/tlsdial"
|
"tailscale.com/net/tlsdial"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/tstime"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/eventbus"
|
"tailscale.com/util/eventbus"
|
||||||
"tailscale.com/util/set"
|
"tailscale.com/util/set"
|
||||||
@@ -175,7 +176,7 @@ func printNetCheckReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printf("\nReport:\n")
|
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)
|
printf("\t* UDP: %v\n", report.UDP)
|
||||||
if report.GlobalV4.IsValid() {
|
if report.GlobalV4.IsValid() {
|
||||||
printf("\t* IPv4: yes, %s\n", report.GlobalV4)
|
printf("\t* IPv4: yes, %s\n", report.GlobalV4)
|
||||||
|
|||||||
@@ -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
|
defer h.b.TryFlushLogs() // kick off upload after bugreport's done logging
|
||||||
|
|
||||||
logMarker := func() string {
|
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() {
|
if envknob.NoLogsNoSupport() {
|
||||||
logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" }
|
logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/tstime"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -106,13 +107,8 @@ func (w *logFileWriter) appendToFileLocked(out []byte) {
|
|||||||
}
|
}
|
||||||
out = removeDatePrefix(out)
|
out = removeDatePrefix(out)
|
||||||
if w.f != nil {
|
if w.f != nil {
|
||||||
// RFC3339Nano but with a fixed number (3) of nanosecond digits:
|
fmt.Fprintf(w.f, "%s: %s",
|
||||||
const formatPre = "2006-01-02T15:04:05"
|
now.Format(tstime.DateTTimeMilliZ),
|
||||||
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),
|
|
||||||
out)
|
out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import (
|
|||||||
"tailscale.com/net/tsdial"
|
"tailscale.com/net/tsdial"
|
||||||
"tailscale.com/sessionrecording"
|
"tailscale.com/sessionrecording"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/tstime"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
@@ -479,7 +480,7 @@ func (srv *server) newConn() (*conn, error) {
|
|||||||
srv.mu.Unlock()
|
srv.mu.Unlock()
|
||||||
c := &conn{srv: srv}
|
c := &conn{srv: srv}
|
||||||
now := srv.now()
|
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{}
|
fwdHandler := &gliderssh.ForwardedTCPHandler{}
|
||||||
streamLocalFwdHandler := &gliderssh.ForwardedUnixHandler{}
|
streamLocalFwdHandler := &gliderssh.ForwardedUnixHandler{}
|
||||||
c.Server = &gliderssh.Server{
|
c.Server = &gliderssh.Server{
|
||||||
@@ -776,7 +777,7 @@ func (ss *sshSession) vlogf(format string, args ...any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) newSSHSession(s gliderssh.Session) *sshSession {
|
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)
|
c.logf("starting session: %v", sharedID)
|
||||||
ctx, cancel := context.WithCancelCause(s.Context())
|
ctx, cancel := context.WithCancelCause(s.Context())
|
||||||
return &sshSession{
|
return &sshSession{
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
+2
-1
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/tstime"
|
||||||
"tailscale.com/util/ctxkey"
|
"tailscale.com/util/ctxkey"
|
||||||
"tailscale.com/util/rands"
|
"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
|
// Now we return "REQ-2" version, where the "2" doubles as the year 2YYY
|
||||||
// in a leading date.
|
// in a leading date.
|
||||||
now := time.Now().UTC()
|
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
|
// SetRequestID is an HTTP middleware that injects a RequestID in the
|
||||||
|
|||||||
Reference in New Issue
Block a user