erigon-pulse/diagnostics/logs.go

191 lines
4.5 KiB
Go
Raw Normal View History

package diagnostics
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"github.com/urfave/cli/v2"
"github.com/ledgerwatch/erigon/turbo/logging"
)
func SetupLogsAccess(ctx *cli.Context, metricsMux *http.ServeMux) {
dirPath := ctx.String(logging.LogDirPathFlag.Name)
if dirPath == "" {
datadir := ctx.String("datadir")
if datadir != "" {
dirPath = filepath.Join(datadir, "logs")
}
}
if dirPath == "" {
return
}
metricsMux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
writeLogsList(w, dirPath)
})
metricsMux.HandleFunc("/logs/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
writeLogsRead(w, r, dirPath)
})
}
func writeLogsList(w http.ResponseWriter, dirPath string) {
entries, err := os.ReadDir(dirPath)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to list directory %s: %v", dirPath, err), http.StatusInternalServerError)
return
}
infos := make([]fs.FileInfo, 0, len(entries))
for _, entry := range entries {
fileInfo, err := os.Stat(filepath.Join(dirPath, entry.Name()))
if err != nil {
http.Error(w, fmt.Sprintf("Can't stat file %s: %v", entry.Name(), err), http.StatusInternalServerError)
return
}
if fileInfo.IsDir() {
continue
}
infos = append(infos, fileInfo)
}
type file struct {
Name string `json:"name"`
Size int64 `json:"size"`
}
files := make([]file, len(infos))
for _, fileInfo := range infos {
files = append(files, file{Name: fileInfo.Name(), Size: fileInfo.Size()})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(files)
}
func writeLogsRead(w http.ResponseWriter, r *http.Request, dirPath string) {
file := path.Base(r.URL.Path)
if file == "/" || file == "." {
http.Error(w, "file is required - specify the name of log file to read", http.StatusBadRequest)
return
}
offset, err := offsetValue(r.URL.Query())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fileInfo, err := os.Stat(filepath.Join(dirPath, file))
if err != nil {
http.Error(w, fmt.Sprintf("Can't stat file %s: %v", file, err), http.StatusInternalServerError)
return
}
if fileInfo.IsDir() {
http.Error(w, fmt.Sprintf("%s is a directory, needs to be a file", file), http.StatusInternalServerError)
return
}
if offset > fileInfo.Size() {
http.Error(w, fmt.Sprintf("offset %d must not be greater than this file size %d", offset, fileInfo.Size()), http.StatusBadRequest)
return
}
f, err := os.Open(filepath.Join(dirPath, file))
if err != nil {
http.Error(w, fmt.Sprintf("Can't opening file %s: %v\n", file, err), http.StatusInternalServerError)
return
}
limit, err := limitValue(r.URL.Query(), fileInfo.Size())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
buf := make([]byte, limit)
if _, err := f.Seek(offset, 0); err != nil {
http.Error(w, fmt.Sprintf("seek failed for file: %s to %d: %v", file, offset, err), http.StatusInternalServerError)
return
}
var n int
var readTotal int
for n, err = f.Read(buf[readTotal:]); err == nil && readTotal < len(buf); n, err = f.Read(buf[readTotal:]) {
readTotal += n
}
if err != nil && !errors.Is(err, io.EOF) {
http.Error(w, fmt.Sprintf("Reading failed for: %s at %d: %v\n", file, readTotal, err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", strconv.FormatInt(int64(readTotal), 10))
w.Header().Set("X-Offset", strconv.FormatInt(offset, 10))
w.Header().Set("X-Limit", strconv.FormatInt(limit, 10))
w.Header().Set("X-Size", strconv.FormatInt(fileInfo.Size(), 10))
w.Write(buf[:readTotal])
}
func limitValue(values url.Values, def int64) (int64, error) {
limitStr := values.Get("limit")
var limit int64
var err error
if limitStr == "" {
limit = def
} else {
limit, err = strconv.ParseInt(limitStr, 10, 64)
}
if err != nil {
return 0, fmt.Errorf("limit %s is not a int64 number: %v", limitStr, err)
}
return limit, nil
}
func offsetValue(values url.Values) (int64, error) {
offsetStr := values.Get("offset")
var offset int64
var err error
if offsetStr != "" {
offset, err = strconv.ParseInt(offsetStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("offset %s is not a int64 number: %v", offsetStr, err)
}
}
if offset < 0 {
return 0, fmt.Errorf("offset %d must be non-negative", offset)
}
return offset, nil
}