log.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // Copyright 2018 The go-ethereum Authors
  2. // This file is part of the go-ethereum library.
  3. //
  4. // The go-ethereum library is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Lesser General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // The go-ethereum library is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Lesser General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Lesser General Public License
  15. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
  16. package dashboard
  17. import (
  18. "bytes"
  19. "encoding/json"
  20. "io/ioutil"
  21. "os"
  22. "path/filepath"
  23. "regexp"
  24. "sort"
  25. "time"
  26. "github.com/ethereum/go-ethereum/log"
  27. "github.com/mohae/deepcopy"
  28. "github.com/rjeczalik/notify"
  29. )
  30. var emptyChunk = json.RawMessage("[]")
  31. // prepLogs creates a JSON array from the given log record buffer.
  32. // Returns the prepared array and the position of the last '\n'
  33. // character in the original buffer, or -1 if it doesn't contain any.
  34. func prepLogs(buf []byte) (json.RawMessage, int) {
  35. b := make(json.RawMessage, 1, len(buf)+1)
  36. b[0] = '['
  37. b = append(b, buf...)
  38. last := -1
  39. for i := 1; i < len(b); i++ {
  40. if b[i] == '\n' {
  41. b[i] = ','
  42. last = i
  43. }
  44. }
  45. if last < 0 {
  46. return emptyChunk, -1
  47. }
  48. b[last] = ']'
  49. return b[:last+1], last - 1
  50. }
  51. // handleLogRequest searches for the log file specified by the timestamp of the
  52. // request, creates a JSON array out of it and sends it to the requesting client.
  53. func (db *Dashboard) handleLogRequest(r *LogsRequest, c *client) {
  54. files, err := ioutil.ReadDir(db.logdir)
  55. if err != nil {
  56. log.Warn("Failed to open logdir", "path", db.logdir, "err", err)
  57. return
  58. }
  59. re := regexp.MustCompile(`\.log$`)
  60. fileNames := make([]string, 0, len(files))
  61. for _, f := range files {
  62. if f.Mode().IsRegular() && re.MatchString(f.Name()) {
  63. fileNames = append(fileNames, f.Name())
  64. }
  65. }
  66. if len(fileNames) < 1 {
  67. log.Warn("No log files in logdir", "path", db.logdir)
  68. return
  69. }
  70. idx := sort.Search(len(fileNames), func(idx int) bool {
  71. // Returns the smallest index such as fileNames[idx] >= r.Name,
  72. // if there is no such index, returns n.
  73. return fileNames[idx] >= r.Name
  74. })
  75. switch {
  76. case idx < 0:
  77. return
  78. case idx == 0 && r.Past:
  79. return
  80. case idx >= len(fileNames):
  81. return
  82. case r.Past:
  83. idx--
  84. case idx == len(fileNames)-1 && fileNames[idx] == r.Name:
  85. return
  86. case idx == len(fileNames)-1 || (idx == len(fileNames)-2 && fileNames[idx] == r.Name):
  87. // The last file is continuously updated, and its chunks are streamed,
  88. // so in order to avoid log record duplication on the client side, it is
  89. // handled differently. Its actual content is always saved in the history.
  90. db.lock.Lock()
  91. if db.history.Logs != nil {
  92. c.msg <- &Message{
  93. Logs: db.history.Logs,
  94. }
  95. }
  96. db.lock.Unlock()
  97. return
  98. case fileNames[idx] == r.Name:
  99. idx++
  100. }
  101. path := filepath.Join(db.logdir, fileNames[idx])
  102. var buf []byte
  103. if buf, err = ioutil.ReadFile(path); err != nil {
  104. log.Warn("Failed to read file", "path", path, "err", err)
  105. return
  106. }
  107. chunk, end := prepLogs(buf)
  108. if end < 0 {
  109. log.Warn("The file doesn't contain valid logs", "path", path)
  110. return
  111. }
  112. c.msg <- &Message{
  113. Logs: &LogsMessage{
  114. Source: &LogFile{
  115. Name: fileNames[idx],
  116. Last: r.Past && idx == 0,
  117. },
  118. Chunk: chunk,
  119. },
  120. }
  121. }
  122. // streamLogs watches the file system, and when the logger writes
  123. // the new log records into the files, picks them up, then makes
  124. // JSON array out of them and sends them to the clients.
  125. func (db *Dashboard) streamLogs() {
  126. defer db.wg.Done()
  127. var (
  128. err error
  129. errc chan error
  130. )
  131. defer func() {
  132. if errc == nil {
  133. errc = <-db.quit
  134. }
  135. errc <- err
  136. }()
  137. files, err := ioutil.ReadDir(db.logdir)
  138. if err != nil {
  139. log.Warn("Failed to open logdir", "path", db.logdir, "err", err)
  140. return
  141. }
  142. var (
  143. opened *os.File // File descriptor for the opened active log file.
  144. buf []byte // Contains the recently written log chunks, which are not sent to the clients yet.
  145. )
  146. // The log records are always written into the last file in alphabetical order, because of the timestamp.
  147. re := regexp.MustCompile(`\.log$`)
  148. i := len(files) - 1
  149. for i >= 0 && (!files[i].Mode().IsRegular() || !re.MatchString(files[i].Name())) {
  150. i--
  151. }
  152. if i < 0 {
  153. log.Warn("No log files in logdir", "path", db.logdir)
  154. return
  155. }
  156. if opened, err = os.OpenFile(filepath.Join(db.logdir, files[i].Name()), os.O_RDONLY, 0600); err != nil {
  157. log.Warn("Failed to open file", "name", files[i].Name(), "err", err)
  158. return
  159. }
  160. defer opened.Close() // Close the lastly opened file.
  161. fi, err := opened.Stat()
  162. if err != nil {
  163. log.Warn("Problem with file", "name", opened.Name(), "err", err)
  164. return
  165. }
  166. db.lock.Lock()
  167. db.history.Logs = &LogsMessage{
  168. Source: &LogFile{
  169. Name: fi.Name(),
  170. Last: true,
  171. },
  172. Chunk: emptyChunk,
  173. }
  174. db.lock.Unlock()
  175. watcher := make(chan notify.EventInfo, 10)
  176. if err := notify.Watch(db.logdir, watcher, notify.Create); err != nil {
  177. log.Warn("Failed to create file system watcher", "err", err)
  178. return
  179. }
  180. defer notify.Stop(watcher)
  181. ticker := time.NewTicker(db.config.Refresh)
  182. defer ticker.Stop()
  183. loop:
  184. for err == nil || errc == nil {
  185. select {
  186. case event := <-watcher:
  187. // Make sure that new log file was created.
  188. if !re.Match([]byte(event.Path())) {
  189. break
  190. }
  191. if opened == nil {
  192. log.Warn("The last log file is not opened")
  193. break loop
  194. }
  195. // The new log file's name is always greater,
  196. // because it is created using the actual log record's time.
  197. if opened.Name() >= event.Path() {
  198. break
  199. }
  200. // Read the rest of the previously opened file.
  201. chunk, err := ioutil.ReadAll(opened)
  202. if err != nil {
  203. log.Warn("Failed to read file", "name", opened.Name(), "err", err)
  204. break loop
  205. }
  206. buf = append(buf, chunk...)
  207. opened.Close()
  208. if chunk, last := prepLogs(buf); last >= 0 {
  209. // Send the rest of the previously opened file.
  210. db.sendToAll(&Message{
  211. Logs: &LogsMessage{
  212. Chunk: chunk,
  213. },
  214. })
  215. }
  216. if opened, err = os.OpenFile(event.Path(), os.O_RDONLY, 0644); err != nil {
  217. log.Warn("Failed to open file", "name", event.Path(), "err", err)
  218. break loop
  219. }
  220. buf = buf[:0]
  221. // Change the last file in the history.
  222. fi, err := opened.Stat()
  223. if err != nil {
  224. log.Warn("Problem with file", "name", opened.Name(), "err", err)
  225. break loop
  226. }
  227. db.lock.Lock()
  228. db.history.Logs.Source.Name = fi.Name()
  229. db.history.Logs.Chunk = emptyChunk
  230. db.lock.Unlock()
  231. case <-ticker.C: // Send log updates to the client.
  232. if opened == nil {
  233. log.Warn("The last log file is not opened")
  234. break loop
  235. }
  236. // Read the new logs created since the last read.
  237. chunk, err := ioutil.ReadAll(opened)
  238. if err != nil {
  239. log.Warn("Failed to read file", "name", opened.Name(), "err", err)
  240. break loop
  241. }
  242. b := append(buf, chunk...)
  243. chunk, last := prepLogs(b)
  244. if last < 0 {
  245. break
  246. }
  247. // Only keep the invalid part of the buffer, which can be valid after the next read.
  248. buf = b[last+1:]
  249. var l *LogsMessage
  250. // Update the history.
  251. db.lock.Lock()
  252. if bytes.Equal(db.history.Logs.Chunk, emptyChunk) {
  253. db.history.Logs.Chunk = chunk
  254. l = deepcopy.Copy(db.history.Logs).(*LogsMessage)
  255. } else {
  256. b = make([]byte, len(db.history.Logs.Chunk)+len(chunk)-1)
  257. copy(b, db.history.Logs.Chunk)
  258. b[len(db.history.Logs.Chunk)-1] = ','
  259. copy(b[len(db.history.Logs.Chunk):], chunk[1:])
  260. db.history.Logs.Chunk = b
  261. l = &LogsMessage{Chunk: chunk}
  262. }
  263. db.lock.Unlock()
  264. db.sendToAll(&Message{Logs: l})
  265. case errc = <-db.quit:
  266. break loop
  267. }
  268. }
  269. }