backend/analyzer/Analyzer.go (309 lines of code) (raw):
package analyzer
import (
"context"
"crypto/sha1"
"embed"
"fmt"
"github.com/nxadm/tail"
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"sort"
"strings"
"sync"
"time"
)
var writeSyncer = sync.Mutex{}
//go:embed *.gohtml
var tmplFS embed.FS
type Analyzer struct {
Context *context.Context
FolderToWorkWith string
IsFolderTemp bool
fileWatchers []*tail.Tail
LastModifiedFileTime time.Time
DynamicEntities DynamicEntities
StaticEntities []StaticEntity
Filters Filters
OtherFiles OtherFiles
AggregatedLogs Logs
AggregatedThreadDumps AggregatedThreadDumps
AggregatedStaticInfo AggregatedStaticInfo
}
type StaticEntity struct {
Name string
ConvertToStaticInfo func(path string) StaticInfo
CheckPath func(path string) bool
CollectedInfo StaticInfo
}
type DynamicEntities []DynamicEntity
//DynamicEntity is a type of log file. Every type is described in separate file of /entities/ folder.
//Every type has set of functions to convert logs of that type to unified logs of IntelliJ Log Analyzer
type DynamicEntity struct {
Name string // Name of the Entity. For example "idea.log", "Thread dump", or "CPU snapshot". It will be used to group same entities.
entityInstances map[string]DynamicEntityProperties // entityInstances is path:DynamicEntityProperties map of every instance of entity created for every found path of this entity type.
ConvertPathToLogs func(path string) Logs //ConvertPathToLogs represents file/folder to the array of log entries.
ConvertStringToLogs func(s string) (LogEntry, error) //ConvertStringToLogs (should be defined for simple log files) represents a string as log entry. Needed when part of a log should be analyzed (for example during tailing process). Returns error if string does not fit log format.
GetChangeablePath func(path string) string //GetChangeablePath (if defined) returns the part of given path, that should be monitored for changes. For simple log file, it is the log file itself. For reports (such as thread dumps) it is directory where new reports are being added
CheckPath func(path string) bool
CheckIgnoredPath func(path string) bool
DefaultVisibility func(path string) bool // DefaultVisibility is a function that returns true if this file should be checked in Filter (visible in "Summary" tab) by default.
GetDisplayName func(path string) string
LineHighlightingColor string //Color represents the color that is used to highlight all lines of this entity type in the editor
}
type DynamicEntityProperties struct {
Hash string
Visible bool
}
func (e *DynamicEntity) addDynamicEntityInstance(path string, visible bool) {
if e.entityInstances == nil {
e.entityInstances = make(map[string]DynamicEntityProperties)
}
e.entityInstances[path] = DynamicEntityProperties{
Hash: getHash(path),
Visible: visible,
}
}
//AddStaticEntity adds new static Entity to the list of known Entities. Should be Called within the application start.
func (a *Analyzer) AddStaticEntity(entity StaticEntity) {
a.StaticEntities = append(a.StaticEntities, entity)
}
//AddDynamicEntity adds new dynamic Entity to the list of known Entities. Should be Called within the application start.
func (a *Analyzer) AddDynamicEntity(entity DynamicEntity) {
a.DynamicEntities = append(a.DynamicEntities, entity)
}
//ParseLogDirectory analyzes provided path for known log elements
func (a *Analyzer) ParseLogDirectory(path string) {
log.Printf("Parsing log directory %s", path)
var wg sync.WaitGroup
var collectedFiles []string
visit := func(path string, file os.DirEntry, err error) error {
wg.Add(1)
go func() {
defer wg.Done()
isDynamic := a.CollectLogsFromDynamicEntities(path)
isStatic := a.CollectStaticInfoFromStaticEntities(path)
writeSyncer.Lock()
if isStatic || isDynamic {
collectedFiles = append(collectedFiles, path)
} else {
if !file.IsDir() && !IsHiddenFile(filepath.Base(path)) {
a.OtherFiles.Append(path)
}
}
writeSyncer.Unlock()
}()
return nil
}
_ = filepath.WalkDir(path, visit)
wg.Wait()
a.OtherFiles = a.OtherFiles.FilterAnalyzedDirectories(collectedFiles)
}
func (a *Analyzer) GetLastModifiedFile() time.Time {
if !a.LastModifiedFileTime.IsZero() {
return a.LastModifiedFileTime
}
var rememberedPath = ""
visit := func(path string, file os.DirEntry, err error) error {
if !file.IsDir() && !IsHiddenFile(filepath.Base(path)) {
if GetFileModTime(path).After(a.LastModifiedFileTime) {
a.LastModifiedFileTime = GetFileModTime(path)
rememberedPath = path
}
}
return nil
}
_ = filepath.WalkDir(a.FolderToWorkWith, visit)
log.Printf("Last modified file: %s timestamp: %s", rememberedPath, a.LastModifiedFileTime)
return a.LastModifiedFileTime
}
//IsEmpty checks if config has at least one filled attribute
func (a *Analyzer) IsEmpty() bool {
return reflect.ValueOf(*a).IsZero()
}
func (a *Analyzer) GetLogs() *Logs {
if !a.AggregatedLogs.IsEmpty() {
return &a.AggregatedLogs
}
return nil
}
func (a *Analyzer) GetStaticInfo() *AggregatedStaticInfo {
if !(len(a.AggregatedStaticInfo) == 0) {
return &a.AggregatedStaticInfo
}
a.AggregatedStaticInfo = aggregateStaticInfo(a.StaticEntities)
return a.GetStaticInfo()
}
func (a *Analyzer) GetOtherFiles() *OtherFiles {
if !a.AggregatedLogs.IsEmpty() {
return &a.OtherFiles
}
return nil
}
//GetThreadDump returns Analyzed ThreadDumps folder as AggregatedThreadDumps entity. Analyzes it if it was not done already.
func (a *Analyzer) GetThreadDump(threadDumpsFolder string) *ThreadDump {
t := a.AggregatedThreadDumps[threadDumpsFolder]
if t != nil {
return &t
}
a.AggregatedThreadDumps[threadDumpsFolder] = make(ThreadDump)
a.AggregatedThreadDumps[threadDumpsFolder] = analyzeThreadDumpsFolder(a.FolderToWorkWith, threadDumpsFolder)
return a.GetThreadDump(threadDumpsFolder)
}
func (a *Analyzer) GetFilters() *Filters {
if !a.Filters.IsEmpty() {
return &a.Filters
}
return nil
}
func (a *Analyzer) CollectStaticInfoFromStaticEntities(path string) (analyzed bool) {
analyzed = false
for i, entity := range a.StaticEntities {
if entity.CheckPath(path) == true {
a.StaticEntities[i].CollectedInfo = entity.ConvertToStaticInfo(path)
analyzed = true
}
}
return analyzed
}
// CollectLogsFromDynamicEntities Checks if path fulfil the Entity requirements and Adds all the Entity's logEntries to the aggregated logs
func (a *Analyzer) CollectLogsFromDynamicEntities(path string) (analyzed bool) {
analyzed = false
for i, entity := range a.DynamicEntities {
if entity.CheckIgnoredPath != nil {
if entity.CheckIgnoredPath(path) == true {
return true
}
}
if entity.CheckPath(path) == true {
logEntries := entity.ConvertPathToLogs(path)
if logEntries == nil {
log.Printf("Entity \"%s\" returned nothing for %s. Adding file to other files", entity.Name, path)
} else {
if entity.DefaultVisibility == nil {
entity.DefaultVisibility = func(path string) bool {
return true
}
}
writeSyncer.Lock()
a.DynamicEntities[i].addDynamicEntityInstance(path, entity.DefaultVisibility(path))
a.AggregatedLogs.AppendSeveral(a.DynamicEntities[i].Name, a.DynamicEntities[i].entityInstances[path], logEntries)
writeSyncer.Unlock()
analyzed = true
}
}
}
return analyzed
}
// GenerateFilters Generates filters for all Entities and saves them into Filters slice
func (a *Analyzer) GenerateFilters() {
filter := a.InitFilter()
for _, entity := range a.DynamicEntities {
for path, _ := range entity.entityInstances {
filter.Append(entity, path)
}
}
filter.SortByFilename()
}
func (a *Analyzer) InitFilter() *Filters {
a.Filters = make(Filters)
return &a.Filters
}
func (a *Analyzer) Clear() {
a.AggregatedLogs = Logs{}
a.Filters = Filters{}
a.OtherFiles = OtherFiles{}
a.AggregatedStaticInfo = AggregatedStaticInfo{}
a.AggregatedThreadDumps = AggregatedThreadDumps{}
a.LastModifiedFileTime = time.Time{}
for i, _ := range a.StaticEntities {
a.StaticEntities[i].CollectedInfo = StaticInfo{}
}
for i, _ := range a.DynamicEntities {
a.DynamicEntities[i].entityInstances = make(map[string]DynamicEntityProperties)
}
if a.IsFolderTemp {
err := os.RemoveAll(a.FolderToWorkWith)
if err != nil {
log.Printf("Removing folder '%s' failed. Error: %s", a.FolderToWorkWith, err)
} else {
log.Printf("Temp folder %s removed", a.FolderToWorkWith)
}
}
a.IsFolderTemp = false
for _, watcher := range a.fileWatchers {
if watcher != nil {
watcher.Stop()
}
}
a.fileWatchers = nil
}
func (a *Analyzer) GetThreadDumps(dir string) Logs {
for _, entity := range a.DynamicEntities {
for path, _ := range entity.entityInstances {
if strings.Contains(path, dir) {
return entity.ConvertPathToLogs(path)
}
}
}
return nil
}
func (e *DynamicEntities) GetInstanceByID(id string) *DynamicEntityProperties {
for _, dynamicEntity := range *e {
for _, entityInstance := range dynamicEntity.entityInstances {
if id == entityInstance.Hash {
return &entityInstance
}
}
}
return nil
}
func aggregateStaticInfo(entity []StaticEntity) (a AggregatedStaticInfo) {
a = make(AggregatedStaticInfo)
for _, staticEntity := range entity {
a[staticEntity.Name] = staticEntity.CollectedInfo
}
return a
}
func getHash(s string) string {
h := sha1.New()
h.Write([]byte(s))
bs := h.Sum(nil)
sh := fmt.Sprintf("%x\n", bs)
return sh
}
/**
* Parses string s with the given regular expression and returns the
* group values defined in the expression.
*
*/
func GetRegexNamedCapturedGroups(regEx, s string) (paramsMap map[string]string) {
var compRegEx = regexp.MustCompile(regEx)
match := compRegEx.FindStringSubmatch(s)
paramsMap = make(map[string]string)
for i, name := range compRegEx.SubexpNames() {
if i > 0 && i <= len(match) {
paramsMap[name] = match[i]
}
}
return paramsMap
}
func sortedKeys[K string, V any](m map[K]V) []K {
keys := make([]K, len(m))
i := 0
for k := range m {
keys[i] = k
i++
}
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
return keys
}
func SliceContains[S comparable](slice []S, element S) int {
for i, e := range slice {
if e == element {
return i
}
}
return -1
}
func IsHiddenFile(filename string) bool {
if runtime.GOOS != "windows" {
return filename[0:1] == "."
}
return false
}
func GetFileModTime(path string) (date time.Time) {
fileinfo, err := os.Stat(path)
if err == nil {
return fileinfo.ModTime()
}
return time.Time{}
}