* cmd, dashboard, internal, log, node: logging feature * cmd, dashboard, internal, log: requested changes * dashboard, vendor: gofmt, govendor, use vendored file watcher * dashboard, log: gofmt -s -w, goimports * dashboard, log: gosimple
		
			
				
	
	
		
			289 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2018 The go-ethereum Authors
 | 
						|
// This file is part of the go-ethereum library.
 | 
						|
//
 | 
						|
// The go-ethereum library is free software: you can redistribute it and/or modify
 | 
						|
// it under the terms of the GNU Lesser General Public License as published by
 | 
						|
// the Free Software Foundation, either version 3 of the License, or
 | 
						|
// (at your option) any later version.
 | 
						|
//
 | 
						|
// The go-ethereum library is distributed in the hope that it will be useful,
 | 
						|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | 
						|
// GNU Lesser General Public License for more details.
 | 
						|
//
 | 
						|
// You should have received a copy of the GNU Lesser General Public License
 | 
						|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
 | 
						|
 | 
						|
package dashboard
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"encoding/json"
 | 
						|
	"io/ioutil"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"regexp"
 | 
						|
	"sort"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/ethereum/go-ethereum/log"
 | 
						|
	"github.com/mohae/deepcopy"
 | 
						|
	"github.com/rjeczalik/notify"
 | 
						|
)
 | 
						|
 | 
						|
var emptyChunk = json.RawMessage("[]")
 | 
						|
 | 
						|
// prepLogs creates a JSON array from the given log record buffer.
 | 
						|
// Returns the prepared array and the position of the last '\n'
 | 
						|
// character in the original buffer, or -1 if it doesn't contain any.
 | 
						|
func prepLogs(buf []byte) (json.RawMessage, int) {
 | 
						|
	b := make(json.RawMessage, 1, len(buf)+1)
 | 
						|
	b[0] = '['
 | 
						|
	b = append(b, buf...)
 | 
						|
	last := -1
 | 
						|
	for i := 1; i < len(b); i++ {
 | 
						|
		if b[i] == '\n' {
 | 
						|
			b[i] = ','
 | 
						|
			last = i
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if last < 0 {
 | 
						|
		return emptyChunk, -1
 | 
						|
	}
 | 
						|
	b[last] = ']'
 | 
						|
	return b[:last+1], last - 1
 | 
						|
}
 | 
						|
 | 
						|
// handleLogRequest searches for the log file specified by the timestamp of the
 | 
						|
// request, creates a JSON array out of it and sends it to the requesting client.
 | 
						|
func (db *Dashboard) handleLogRequest(r *LogsRequest, c *client) {
 | 
						|
	files, err := ioutil.ReadDir(db.logdir)
 | 
						|
	if err != nil {
 | 
						|
		log.Warn("Failed to open logdir", "path", db.logdir, "err", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	re := regexp.MustCompile(`\.log$`)
 | 
						|
	fileNames := make([]string, 0, len(files))
 | 
						|
	for _, f := range files {
 | 
						|
		if f.Mode().IsRegular() && re.MatchString(f.Name()) {
 | 
						|
			fileNames = append(fileNames, f.Name())
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if len(fileNames) < 1 {
 | 
						|
		log.Warn("No log files in logdir", "path", db.logdir)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	idx := sort.Search(len(fileNames), func(idx int) bool {
 | 
						|
		// Returns the smallest index such as fileNames[idx] >= r.Name,
 | 
						|
		// if there is no such index, returns n.
 | 
						|
		return fileNames[idx] >= r.Name
 | 
						|
	})
 | 
						|
 | 
						|
	switch {
 | 
						|
	case idx < 0:
 | 
						|
		return
 | 
						|
	case idx == 0 && r.Past:
 | 
						|
		return
 | 
						|
	case idx >= len(fileNames):
 | 
						|
		return
 | 
						|
	case r.Past:
 | 
						|
		idx--
 | 
						|
	case idx == len(fileNames)-1 && fileNames[idx] == r.Name:
 | 
						|
		return
 | 
						|
	case idx == len(fileNames)-1 || (idx == len(fileNames)-2 && fileNames[idx] == r.Name):
 | 
						|
		// The last file is continuously updated, and its chunks are streamed,
 | 
						|
		// so in order to avoid log record duplication on the client side, it is
 | 
						|
		// handled differently. Its actual content is always saved in the history.
 | 
						|
		db.lock.Lock()
 | 
						|
		if db.history.Logs != nil {
 | 
						|
			c.msg <- &Message{
 | 
						|
				Logs: db.history.Logs,
 | 
						|
			}
 | 
						|
		}
 | 
						|
		db.lock.Unlock()
 | 
						|
		return
 | 
						|
	case fileNames[idx] == r.Name:
 | 
						|
		idx++
 | 
						|
	}
 | 
						|
 | 
						|
	path := filepath.Join(db.logdir, fileNames[idx])
 | 
						|
	var buf []byte
 | 
						|
	if buf, err = ioutil.ReadFile(path); err != nil {
 | 
						|
		log.Warn("Failed to read file", "path", path, "err", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	chunk, end := prepLogs(buf)
 | 
						|
	if end < 0 {
 | 
						|
		log.Warn("The file doesn't contain valid logs", "path", path)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	c.msg <- &Message{
 | 
						|
		Logs: &LogsMessage{
 | 
						|
			Source: &LogFile{
 | 
						|
				Name: fileNames[idx],
 | 
						|
				Last: r.Past && idx == 0,
 | 
						|
			},
 | 
						|
			Chunk: chunk,
 | 
						|
		},
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// streamLogs watches the file system, and when the logger writes
 | 
						|
// the new log records into the files, picks them up, then makes
 | 
						|
// JSON array out of them and sends them to the clients.
 | 
						|
func (db *Dashboard) streamLogs() {
 | 
						|
	defer db.wg.Done()
 | 
						|
	var (
 | 
						|
		err  error
 | 
						|
		errc chan error
 | 
						|
	)
 | 
						|
	defer func() {
 | 
						|
		if errc == nil {
 | 
						|
			errc = <-db.quit
 | 
						|
		}
 | 
						|
		errc <- err
 | 
						|
	}()
 | 
						|
 | 
						|
	files, err := ioutil.ReadDir(db.logdir)
 | 
						|
	if err != nil {
 | 
						|
		log.Warn("Failed to open logdir", "path", db.logdir, "err", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	var (
 | 
						|
		opened *os.File // File descriptor for the opened active log file.
 | 
						|
		buf    []byte   // Contains the recently written log chunks, which are not sent to the clients yet.
 | 
						|
	)
 | 
						|
 | 
						|
	// The log records are always written into the last file in alphabetical order, because of the timestamp.
 | 
						|
	re := regexp.MustCompile(`\.log$`)
 | 
						|
	i := len(files) - 1
 | 
						|
	for i >= 0 && (!files[i].Mode().IsRegular() || !re.MatchString(files[i].Name())) {
 | 
						|
		i--
 | 
						|
	}
 | 
						|
	if i < 0 {
 | 
						|
		log.Warn("No log files in logdir", "path", db.logdir)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if opened, err = os.OpenFile(filepath.Join(db.logdir, files[i].Name()), os.O_RDONLY, 0600); err != nil {
 | 
						|
		log.Warn("Failed to open file", "name", files[i].Name(), "err", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	defer opened.Close() // Close the lastly opened file.
 | 
						|
	fi, err := opened.Stat()
 | 
						|
	if err != nil {
 | 
						|
		log.Warn("Problem with file", "name", opened.Name(), "err", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	db.lock.Lock()
 | 
						|
	db.history.Logs = &LogsMessage{
 | 
						|
		Source: &LogFile{
 | 
						|
			Name: fi.Name(),
 | 
						|
			Last: true,
 | 
						|
		},
 | 
						|
		Chunk: emptyChunk,
 | 
						|
	}
 | 
						|
	db.lock.Unlock()
 | 
						|
 | 
						|
	watcher := make(chan notify.EventInfo, 10)
 | 
						|
	if err := notify.Watch(db.logdir, watcher, notify.Create); err != nil {
 | 
						|
		log.Warn("Failed to create file system watcher", "err", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	defer notify.Stop(watcher)
 | 
						|
 | 
						|
	ticker := time.NewTicker(db.config.Refresh)
 | 
						|
	defer ticker.Stop()
 | 
						|
 | 
						|
loop:
 | 
						|
	for err == nil || errc == nil {
 | 
						|
		select {
 | 
						|
		case event := <-watcher:
 | 
						|
			// Make sure that new log file was created.
 | 
						|
			if !re.Match([]byte(event.Path())) {
 | 
						|
				break
 | 
						|
			}
 | 
						|
			if opened == nil {
 | 
						|
				log.Warn("The last log file is not opened")
 | 
						|
				break loop
 | 
						|
			}
 | 
						|
			// The new log file's name is always greater,
 | 
						|
			// because it is created using the actual log record's time.
 | 
						|
			if opened.Name() >= event.Path() {
 | 
						|
				break
 | 
						|
			}
 | 
						|
			// Read the rest of the previously opened file.
 | 
						|
			chunk, err := ioutil.ReadAll(opened)
 | 
						|
			if err != nil {
 | 
						|
				log.Warn("Failed to read file", "name", opened.Name(), "err", err)
 | 
						|
				break loop
 | 
						|
			}
 | 
						|
			buf = append(buf, chunk...)
 | 
						|
			opened.Close()
 | 
						|
 | 
						|
			if chunk, last := prepLogs(buf); last >= 0 {
 | 
						|
				// Send the rest of the previously opened file.
 | 
						|
				db.sendToAll(&Message{
 | 
						|
					Logs: &LogsMessage{
 | 
						|
						Chunk: chunk,
 | 
						|
					},
 | 
						|
				})
 | 
						|
			}
 | 
						|
			if opened, err = os.OpenFile(event.Path(), os.O_RDONLY, 0644); err != nil {
 | 
						|
				log.Warn("Failed to open file", "name", event.Path(), "err", err)
 | 
						|
				break loop
 | 
						|
			}
 | 
						|
			buf = buf[:0]
 | 
						|
 | 
						|
			// Change the last file in the history.
 | 
						|
			fi, err := opened.Stat()
 | 
						|
			if err != nil {
 | 
						|
				log.Warn("Problem with file", "name", opened.Name(), "err", err)
 | 
						|
				break loop
 | 
						|
			}
 | 
						|
			db.lock.Lock()
 | 
						|
			db.history.Logs.Source.Name = fi.Name()
 | 
						|
			db.history.Logs.Chunk = emptyChunk
 | 
						|
			db.lock.Unlock()
 | 
						|
		case <-ticker.C: // Send log updates to the client.
 | 
						|
			if opened == nil {
 | 
						|
				log.Warn("The last log file is not opened")
 | 
						|
				break loop
 | 
						|
			}
 | 
						|
			// Read the new logs created since the last read.
 | 
						|
			chunk, err := ioutil.ReadAll(opened)
 | 
						|
			if err != nil {
 | 
						|
				log.Warn("Failed to read file", "name", opened.Name(), "err", err)
 | 
						|
				break loop
 | 
						|
			}
 | 
						|
			b := append(buf, chunk...)
 | 
						|
 | 
						|
			chunk, last := prepLogs(b)
 | 
						|
			if last < 0 {
 | 
						|
				break
 | 
						|
			}
 | 
						|
			// Only keep the invalid part of the buffer, which can be valid after the next read.
 | 
						|
			buf = b[last+1:]
 | 
						|
 | 
						|
			var l *LogsMessage
 | 
						|
			// Update the history.
 | 
						|
			db.lock.Lock()
 | 
						|
			if bytes.Equal(db.history.Logs.Chunk, emptyChunk) {
 | 
						|
				db.history.Logs.Chunk = chunk
 | 
						|
				l = deepcopy.Copy(db.history.Logs).(*LogsMessage)
 | 
						|
			} else {
 | 
						|
				b = make([]byte, len(db.history.Logs.Chunk)+len(chunk)-1)
 | 
						|
				copy(b, db.history.Logs.Chunk)
 | 
						|
				b[len(db.history.Logs.Chunk)-1] = ','
 | 
						|
				copy(b[len(db.history.Logs.Chunk):], chunk[1:])
 | 
						|
				db.history.Logs.Chunk = b
 | 
						|
				l = &LogsMessage{Chunk: chunk}
 | 
						|
			}
 | 
						|
			db.lock.Unlock()
 | 
						|
 | 
						|
			db.sendToAll(&Message{Logs: l})
 | 
						|
		case errc = <-db.quit:
 | 
						|
			break loop
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |