Created
November 16, 2022 02:25
-
-
Save pclose/8a9fcaffc27cc7c1b7a7bbb11193d82e to your computer and use it in GitHub Desktop.
This program watches for changes in the Microfocus Sitescope OM integration log and forwards new events to a Moogsoft REST LAM -pete 2021-09-17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "os" | |
| "time" | |
| "fmt" | |
| "bufio" | |
| "strings" | |
| "net/http" | |
| "bytes" | |
| "encoding/json" | |
| "crypto/tls" | |
| "path/filepath" | |
| "strconv" | |
| "io" | |
| "flag" | |
| "regexp" | |
| "golang.org/x/sys/windows/svc" | |
| "golang.org/x/sys/windows/svc/debug" | |
| "golang.org/x/sys/windows/svc/eventlog" | |
| "golang.org/x/sys/windows/svc/mgr" | |
| ) | |
| func main () { | |
| exen := filepath.Base(os.Args[0]) | |
| sname := strings.Replace(exen, ".exe" ,"" ,1) | |
| var custom_service_name string | |
| var install,uninstall bool | |
| flag.BoolVar(&install, "install", false, "Optional flag for installing as a service") | |
| flag.BoolVar(&uninstall, "uninstall", false, "Optional flag for uninstalling as a service") | |
| flag.StringVar(&custom_service_name, "sname", "", "Set an alternative service name. This can be used to run multiple services") | |
| flag.Parse() | |
| if flag.NArg() < 2 { | |
| flag.PrintDefaults() | |
| fmt.Println("This program watches for changes in the Microfocus Sitescope OM integration log and forwards new events to a Moogsoft REST LAM -pete 2021-09-17") | |
| fmt.Printf("Usage: %s [-install|-uninstall] [-sname <service name>] <logfile> <url>\n", exen) | |
| os.Exit(1) | |
| } | |
| var logfile = flag.Arg(0) | |
| var url = flag.Arg(1) | |
| var is_custom_name = false | |
| if custom_service_name != "" { | |
| sname = custom_service_name | |
| is_custom_name = true | |
| } | |
| inService, err := svc.IsWindowsService() | |
| if err != nil { | |
| fmt.Printf("failed to determine if we are running in service: %v", err) | |
| os.Exit(1) | |
| } | |
| if inService { | |
| runService(logfile, url, false, sname, is_custom_name) | |
| return | |
| } | |
| if install || uninstall { | |
| var err error | |
| if install { | |
| err = installService(sname, logfile, url, is_custom_name) | |
| } | |
| if uninstall { | |
| err = removeService(sname) | |
| } | |
| if err != nil { | |
| fmt.Printf("Command failed: %s\n", err) | |
| } | |
| return | |
| } | |
| runService(logfile, url, true, sname, is_custom_name) | |
| } | |
| var elog debug.Log | |
| func runService(logfile string, url string, isDebug bool, sname string, is_custom_name bool) { | |
| var err error | |
| event_log_name := strings.ReplaceAll(sname, "-", " ") | |
| if isDebug { | |
| elog = debug.New(event_log_name) | |
| } else { | |
| elog, err = eventlog.Open(event_log_name) | |
| if err != nil { | |
| return | |
| } | |
| } | |
| defer elog.Close() | |
| elog.Info(1, fmt.Sprintf("starting %s service", sname)) | |
| run := svc.Run | |
| if isDebug { | |
| run = debug.Run | |
| } | |
| err = run(sname, &win_service{logfile, url, sname, is_custom_name}) | |
| if err != nil { | |
| elog.Error(1, fmt.Sprintf("%s service failed: %v", sname, err)) | |
| return | |
| } | |
| elog.Info(1, fmt.Sprintf("%s service stopped", sname)) | |
| } | |
| type win_service struct{ | |
| logfile string | |
| url string | |
| sname string | |
| is_custom_name bool | |
| } | |
| func (m *win_service) Execute(args []string, requests <-chan svc.ChangeRequest, changes chan <- svc.Status) (ssec bool, errno uint32) { | |
| const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | |
| changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} | |
| stopsig := make(chan bool) | |
| stoppedsig := make(chan bool) | |
| go tailLog(m.logfile, m.url, stopsig, m.sname, m.is_custom_name, stoppedsig) | |
| for { | |
| select { | |
| case c := <-requests: | |
| switch c.Cmd { | |
| case svc.Interrogate: | |
| changes <- c.CurrentStatus | |
| case svc.Stop, svc.Shutdown: { | |
| stopsig <- true | |
| elog.Info(1, fmt.Sprintf("processing stop request")) | |
| changes <- svc.Status{State: svc.StopPending} | |
| time.Sleep(time.Duration(SLEEPS*2) * time.Second) | |
| changes <- svc.Status{State: svc.Stopped} | |
| return | |
| } | |
| default: | |
| elog.Error(1, fmt.Sprintf("unexpected control request #%d", c)) | |
| } | |
| case <- stoppedsig: | |
| changes <- svc.Status{State: svc.Stopped} | |
| return | |
| } | |
| } | |
| return | |
| } | |
| var SLEEPS int = 5 | |
| func tailLog(logfile string, url string, stopsig <- chan bool, sname string, is_custom_name bool, stoppedsig chan <- bool) { | |
| agent_location,_ := os.Hostname() | |
| tr := &http.Transport{ // TODO: load the system CA certs | |
| TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, | |
| } | |
| client := &http.Client{Transport: tr} | |
| sevmap := map[string]int{ | |
| "NORMAL":0, | |
| "GOOD":0, | |
| "UNKNOWN":1, | |
| "WARNING":2, | |
| "MINOR":3, | |
| "MAJOR":4, | |
| "ERROR":4, | |
| "CRITICAL":5, | |
| } | |
| exen := filepath.Base(os.Args[0]) | |
| path,_ := filepath.Abs(os.Args[0]) | |
| dir := filepath.Dir(path) | |
| var seekfn,agent string | |
| if is_custom_name { | |
| seekfn = fmt.Sprintf("%s.%s.seek", strings.Replace(exen, ".exe", "", 1), sname) | |
| agent = fmt.Sprintf("%s:%s", exen, sname) | |
| } else { | |
| seekfn = fmt.Sprintf("%s.seek", sname) | |
| agent = fmt.Sprintf("%s", exen) | |
| } | |
| seekfp := filepath.Join(dir, seekfn) | |
| size := readSeek(seekfp) | |
| // Main loop waits for the file to change size | |
| for true { | |
| select { | |
| case <-stopsig: | |
| stoppedsig <- true | |
| return | |
| default: { | |
| fstat,err := os.Stat(logfile) | |
| if err != nil { | |
| elog.Error(2,fmt.Sprintf("Error checking logfile stats: %s, %s", logfile, err)) | |
| stoppedsig <- true | |
| return | |
| } | |
| var csize = fstat.Size() | |
| // Initial state | |
| if size < 0 { | |
| size = csize | |
| continue | |
| } | |
| // | |
| if csize != size { | |
| offset := procLog(size, csize, logfile, url, agent_location, client, sevmap, agent) | |
| writeSeek(seekfp, offset) | |
| } | |
| size = csize | |
| time.Sleep(time.Duration(SLEEPS) * time.Second) | |
| } | |
| } | |
| } | |
| } | |
| func readSeek(seekfp string) int64 { | |
| _,err := os.Stat(seekfp) | |
| if err != nil { | |
| return -1 // File doesn't exist, init seek file on next writeSeek call | |
| } | |
| content, err := os.ReadFile(seekfp) | |
| if err != nil { | |
| elog.Error(2, fmt.Sprintf("Error opening seekfile: %s, %s", seekfp, err)) | |
| return -1 // Seekfile isn't readable for some reason, make sure not to read whole logfile | |
| } | |
| r := strings.NewReplacer("\n","", "\r","") | |
| size,err := strconv.ParseInt(r.Replace(string(content)), 10, 64) | |
| if err != nil { | |
| elog.Error(2, fmt.Sprintf("Error reading seekfile contents: %s, filepath: %s, err: %s", content, seekfp, err)) | |
| return -1 | |
| } | |
| return size | |
| } | |
| func writeSeek(seekfp string, size int64) { | |
| data := []byte(fmt.Sprintf("%d", size)) | |
| err := os.WriteFile(seekfp, data, 0644) | |
| if err != nil { | |
| elog.Warning(2, fmt.Sprintf("Error writing seekfile contents: %s, filepath: %s, err: %s", data, seekfp, err)) | |
| } | |
| } | |
| func procLog(size int64, csize int64, logfile string, url string, agent_location string, client *http.Client, sevmap map[string]int, agent string) int64 { | |
| if (csize - size) < 1 {size = 0}// File has been overwritten | |
| // Unfortunately the existing OM integration logfile does not roll-over | |
| f, err := os.Open(logfile) | |
| if err != nil { | |
| elog.Error(2, fmt.Sprintf("Error opening logfile: %s, %s", logfile, err)) | |
| return size | |
| } | |
| defer f.Close() | |
| var offset = size | |
| f.Seek(size, io.SeekStart) | |
| nliner := strings.NewReplacer("\n","", "\r","") | |
| reader := bufio.NewReader(f) | |
| var line string | |
| var cerr error | |
| for { | |
| line, cerr = reader.ReadString('\n') | |
| rlen := int64(len(line)) | |
| if rlen < 1 {break} | |
| record := nliner.Replace(line) | |
| rbody, err1 := procRecord(record, agent_location, sevmap, logfile, agent) | |
| if err1 != nil { | |
| elog.Error(2, fmt.Sprintf("Error processing record at %d byte offset in logfile: %s, err: %s", offset, logfile, err1)) | |
| offset += rlen | |
| continue | |
| } | |
| offset += rlen | |
| _, err2 := client.Post(url, "application/json", bytes.NewBuffer(rbody)) | |
| if err2 != nil { | |
| elog.Error(2, fmt.Sprintf("Error sending http data: %s", err2)) | |
| offset -= rlen | |
| return offset | |
| } | |
| if cerr != nil {break} | |
| } | |
| return offset | |
| } | |
| var dtre =regexp.MustCompile(`\d\d\d\d/\d\d/\d\d \d\d:\d\d:\d\d`) | |
| func procRecord(text string, agent_location string, sevmap map[string]int, logfile string, agent string) ([]byte, error) { | |
| var result []byte | |
| var rerr error | |
| defer func() ([]byte,error){ | |
| if r:=recover(); r != nil{ | |
| fmt.Println(r) | |
| rerr = fmt.Errorf("Something went wrong parsing record, err: %s, data: %s", r, text) | |
| return result,rerr | |
| } | |
| return result,rerr | |
| }() | |
| var fields = strings.Split(text, "\t") | |
| if len(fields) < 17 { | |
| err := fmt.Errorf("Logfile record not long enough len: %d, values: %s", len(fields), fields) | |
| return nil, err | |
| } | |
| rdata := map[string]interface{}{ | |
| // These 4 values make up the default signature: source_id, manager, agent, class | |
| "source_id": fields[16], // Internal ID of the monitor/alert | |
| "manager": "Sitescope", | |
| "agent": agent, // Executable + custom service name | |
| "class": logfile, // Logfile | |
| "agent_location": agent_location, // Hostname of local machine | |
| "severity": sevmap[fields[1]], | |
| "external_id": fields[3], // Path in Sitescope inventory | |
| "source": fields[4], // Hostname reported as monitoring target | |
| "description": fields[5], // Name of monitor + status (typically) | |
| "type": fields[7], // Monitor type | |
| "category": fields[8], // Name of counter being alerted on | |
| "url":fields[15], // Drill down URL | |
| "full_description":fields[5]+"\n"+fields[6], | |
| } | |
| var timestamp = fields[0][:strings.LastIndexByte(fields[0],':')] // Strip trailing millisecond, golang doesn't like this format | |
| tzloc,_ := time.LoadLocation("Local") | |
| if !dtre.Match([]byte(timestamp)) {return nil, fmt.Errorf("Bad timestamp: %s", timestamp)} | |
| dt,_ := time.ParseInLocation("2006/01/02 15:04:05", timestamp, tzloc) | |
| rdata["agent_time"] = dt.Unix() // Convert to unix epoch timestamp | |
| result,rerr = json.Marshal(rdata) | |
| return result, rerr | |
| } | |
| func exePath() (string, error) { | |
| prog := os.Args[0] | |
| p, err := filepath.Abs(prog) | |
| if err != nil { | |
| return "", err | |
| } | |
| fi, err := os.Stat(p) | |
| if err == nil { | |
| if !fi.Mode().IsDir() { | |
| return p, nil | |
| } | |
| err = fmt.Errorf("%s is directory", p) | |
| } | |
| if filepath.Ext(p) == "" { | |
| p += ".exe" | |
| fi, err := os.Stat(p) | |
| if err == nil { | |
| if !fi.Mode().IsDir() { | |
| return p, nil | |
| } | |
| err = fmt.Errorf("%s is directory", p) | |
| } | |
| } | |
| return "", err | |
| } | |
| func installService(name string, logfile string, url string, is_custom_name bool) error { | |
| exepath, err := exePath() | |
| if err != nil { | |
| return err | |
| } | |
| m, err := mgr.Connect() | |
| if err != nil { | |
| return err | |
| } | |
| defer m.Disconnect() | |
| s, err := m.OpenService(name) | |
| if err == nil { | |
| s.Close() | |
| return fmt.Errorf("service %s already exists", name) | |
| } | |
| if is_custom_name { | |
| s, err = m.CreateService(name, exepath, mgr.Config{DisplayName: name, StartType: mgr.StartAutomatic}, "-sname", name, logfile, url) | |
| } else { | |
| s, err = m.CreateService(name, exepath, mgr.Config{DisplayName: name, StartType: mgr.StartAutomatic}, logfile, url) | |
| } | |
| if err != nil { | |
| return err | |
| } | |
| defer s.Close() | |
| event_log_name := strings.ReplaceAll(name, "-", " ") | |
| err = eventlog.InstallAsEventCreate(event_log_name, eventlog.Error|eventlog.Warning|eventlog.Info) | |
| if err != nil { | |
| s.Delete() | |
| return fmt.Errorf("SetupEventLogSource() failed: %s", err) | |
| } | |
| return nil | |
| } | |
| func removeService(name string) error { | |
| m, err := mgr.Connect() | |
| if err != nil { | |
| return err | |
| } | |
| defer m.Disconnect() | |
| s, err := m.OpenService(name) | |
| if err != nil { | |
| return fmt.Errorf("service %s is not installed", name) | |
| } | |
| defer s.Close() | |
| err = s.Delete() | |
| if err != nil { | |
| return err | |
| } | |
| event_log_name := strings.ReplaceAll(name, "-", " ") | |
| err = eventlog.Remove(event_log_name) | |
| if err != nil { | |
| return fmt.Errorf("RemoveEventLogSource() failed: %s", err) | |
| } | |
| return nil | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment