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 | ||
|  | 		} | ||
|  | 	} | ||
|  | } |