Skip to content

Instantly share code, notes, and snippets.

@pclose
Created November 16, 2022 02:25
Show Gist options
  • Select an option

  • Save pclose/8a9fcaffc27cc7c1b7a7bbb11193d82e to your computer and use it in GitHub Desktop.

Select an option

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