log.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package filelogger provides localdisk log writing & rotation, primarily for Windows
  4. // clients. (We get this for free on other platforms.)
  5. package filelogger
  6. import (
  7. "bytes"
  8. "fmt"
  9. "log"
  10. "os"
  11. "path/filepath"
  12. "runtime"
  13. "strings"
  14. "sync"
  15. "time"
  16. "tailscale.com/types/logger"
  17. )
  18. const (
  19. maxSize = 100 << 20
  20. maxFiles = 50
  21. )
  22. // New returns a logf wrapper that appends to local disk log
  23. // files on Windows, rotating old log files as needed to stay under
  24. // file count & byte limits.
  25. func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
  26. if runtime.GOOS != "windows" {
  27. panic("not yet supported on any platform except Windows")
  28. }
  29. if logf == nil {
  30. panic("nil logf")
  31. }
  32. dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs")
  33. if err := os.MkdirAll(dir, 0700); err != nil {
  34. log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
  35. return logf
  36. }
  37. logf("local disk logdir: %v", dir)
  38. lfw := &logFileWriter{
  39. fileBasePrefix: fileBasePrefix,
  40. logID: logID,
  41. dir: dir,
  42. wrappedLogf: logf,
  43. }
  44. return lfw.Logf
  45. }
  46. // logFileWriter is the state for the log writer & rotator.
  47. type logFileWriter struct {
  48. dir string // e.g. `C:\Users\FooBarUser\AppData\Local\Tailscale\Logs`
  49. logID string // hex logID
  50. fileBasePrefix string // e.g. "tailscale-service" or "tailscale-gui"
  51. wrappedLogf logger.Logf // underlying logger to send to
  52. mu sync.Mutex // guards following
  53. buf bytes.Buffer // scratch buffer to avoid allocs
  54. fday civilDay // day that f was opened; zero means no file yet open
  55. f *os.File // file currently opened for append
  56. }
  57. // civilDay is a year, month, and day in the local timezone.
  58. // It's a comparable value type.
  59. type civilDay struct {
  60. year int
  61. month time.Month
  62. day int
  63. }
  64. func dayOf(t time.Time) civilDay {
  65. return civilDay{t.Year(), t.Month(), t.Day()}
  66. }
  67. func (w *logFileWriter) Logf(format string, a ...any) {
  68. w.mu.Lock()
  69. defer w.mu.Unlock()
  70. w.buf.Reset()
  71. fmt.Fprintf(&w.buf, format, a...)
  72. if w.buf.Len() == 0 {
  73. return
  74. }
  75. out := w.buf.Bytes()
  76. w.wrappedLogf("%s", out)
  77. // Make sure there's a final newline before we write to the log file.
  78. if out[len(out)-1] != '\n' {
  79. w.buf.WriteByte('\n')
  80. out = w.buf.Bytes()
  81. }
  82. w.appendToFileLocked(out)
  83. }
  84. // out should end in a newline.
  85. // w.mu must be held.
  86. func (w *logFileWriter) appendToFileLocked(out []byte) {
  87. now := time.Now()
  88. day := dayOf(now)
  89. if w.fday != day {
  90. w.startNewFileLocked()
  91. }
  92. out = removeDatePrefix(out)
  93. if w.f != nil {
  94. // RFC3339Nano but with a fixed number (3) of nanosecond digits:
  95. const formatPre = "2006-01-02T15:04:05"
  96. const formatPost = "Z07:00"
  97. fmt.Fprintf(w.f, "%s.%03d%s: %s",
  98. now.Format(formatPre),
  99. now.Nanosecond()/int(time.Millisecond/time.Nanosecond),
  100. now.Format(formatPost),
  101. out)
  102. }
  103. }
  104. func isNum(b byte) bool { return '0' <= b && b <= '9' }
  105. // removeDatePrefix returns a subslice of v with the log package's
  106. // standard datetime prefix format removed, if present.
  107. func removeDatePrefix(v []byte) []byte {
  108. const format = "2009/01/23 01:23:23 "
  109. if len(v) < len(format) {
  110. return v
  111. }
  112. for i, b := range v[:len(format)] {
  113. fb := format[i]
  114. if isNum(fb) {
  115. if !isNum(b) {
  116. return v
  117. }
  118. continue
  119. }
  120. if b != fb {
  121. return v
  122. }
  123. }
  124. return v[len(format):]
  125. }
  126. // startNewFileLocked opens a new log file for writing
  127. // and also cleans up any old files.
  128. //
  129. // w.mu must be held.
  130. func (w *logFileWriter) startNewFileLocked() {
  131. var oldName string
  132. if w.f != nil {
  133. oldName = filepath.Base(w.f.Name())
  134. w.f.Close()
  135. w.f = nil
  136. w.fday = civilDay{}
  137. }
  138. w.cleanLocked()
  139. now := time.Now()
  140. day := dayOf(now)
  141. name := filepath.Join(w.dir, fmt.Sprintf("%s-%04d%02d%02dT%02d%02d%02d-%d.txt",
  142. w.fileBasePrefix,
  143. day.year,
  144. day.month,
  145. day.day,
  146. now.Hour(),
  147. now.Minute(),
  148. now.Second(),
  149. now.Unix()))
  150. var err error
  151. w.f, err = os.Create(name)
  152. if err != nil {
  153. w.wrappedLogf("failed to create log file: %v", err)
  154. return
  155. }
  156. if oldName != "" {
  157. fmt.Fprintf(w.f, "(logID %q; continued from log file %s)\n", w.logID, oldName)
  158. } else {
  159. fmt.Fprintf(w.f, "(logID %q)\n", w.logID)
  160. }
  161. w.fday = day
  162. }
  163. // cleanLocked cleans up old log files.
  164. //
  165. // w.mu must be held.
  166. func (w *logFileWriter) cleanLocked() {
  167. entries, _ := os.ReadDir(w.dir)
  168. prefix := w.fileBasePrefix + "-"
  169. fileSize := map[string]int64{}
  170. var files []string
  171. var sumSize int64
  172. for _, entry := range entries {
  173. fi, err := entry.Info()
  174. if err != nil {
  175. w.wrappedLogf("error getting log file info: %v", err)
  176. continue
  177. }
  178. baseName := filepath.Base(fi.Name())
  179. if !strings.HasPrefix(baseName, prefix) {
  180. continue
  181. }
  182. size := fi.Size()
  183. fileSize[baseName] = size
  184. sumSize += size
  185. files = append(files, baseName)
  186. }
  187. if sumSize > maxSize {
  188. w.wrappedLogf("cleaning log files; sum byte count %d > %d", sumSize, maxSize)
  189. }
  190. if len(files) > maxFiles {
  191. w.wrappedLogf("cleaning log files; number of files %d > %d", len(files), maxFiles)
  192. }
  193. for (sumSize > maxSize || len(files) > maxFiles) && len(files) > 0 {
  194. target := files[0]
  195. files = files[1:]
  196. targetSize := fileSize[target]
  197. targetFull := filepath.Join(w.dir, target)
  198. err := os.Remove(targetFull)
  199. if err != nil {
  200. w.wrappedLogf("error cleaning log file: %v", err)
  201. } else {
  202. sumSize -= targetSize
  203. w.wrappedLogf("cleaned log file %s (size %d); new bytes=%v, files=%v", targetFull, targetSize, sumSize, len(files))
  204. }
  205. }
  206. }