github.com/openethereum/go-ethereum@v1.9.7/dashboard/log.go (about)

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