big rework for settings. plus fixes found during testing

This commit is contained in:
2023-04-18 00:06:10 -07:00
parent d0802de7ae
commit 8de6750dca
2 changed files with 527 additions and 338 deletions

552
arma.go
View File

@@ -9,13 +9,16 @@ package main
import "C" // This is required to import the C code import "C" // This is required to import the C code
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io/ioutil"
"log" "log"
"os" "os"
"reflect" "reflect"
"strconv"
"strings" "strings"
"time" "time"
"unsafe" "unsafe"
@@ -27,31 +30,87 @@ import (
// declare list of functions available for call // declare list of functions available for call
var AVAILABLE_FUNCTIONS = map[string]interface{}{ var AVAILABLE_FUNCTIONS = map[string]interface{}{
"initExtension": initExtension,
"deinitExtension": deinitExtension,
"loadSettings": loadSettings, "loadSettings": loadSettings,
"connectToInflux": connectToInflux, "connectToInflux": connectToInflux,
"writeToInflux": writeToInflux, "writeToInflux": writeToInflux,
"connectToTimescale": connectToTimescale, "connectToTimescale": connectToTimescale,
"initTimescale": initTimescale, "initTimescale": initTimescale,
"writeToTimescale": writeToTimescale, "writeToTimescale": writeToTimescale,
"deinitialize": deinitialize,
"getDir": getDir, "getDir": getDir,
"sanitizeLineProtocol": sanitizeLineProtocol, "sanitizeLineProtocol": sanitizeLineProtocol,
"version": version,
"getUnixTimeNano": getUnixTimeNano,
} }
var EXTENSION_VERSION string = "0.0.1" var EXTENSION_VERSION string = "0.0.1"
var extensionCallbackFnc C.extensionCallback var extensionCallbackFnc C.extensionCallback
var influxConnectionSettings influxSettings
var a3Settings arma3Settings type ServerPollSetting struct {
var timescaleConnectionSettings timescaleSettings Name string `json:"name"`
var timescaleDbPool *pgxpool.Pool Enabled bool `json:"enabled"`
ServerOnly bool `json:"serverOnly"`
IntervalMs int `json:"intervalMs"`
Bucket string `json:"bucket"`
Measurement string `json:"measurement"`
Description string `json:"description"`
}
var ServerPollSettingProperties []string = []string{
"Name",
"Enabled",
"ServerOnly",
"IntervalMs",
"Bucket",
"Measurement",
"Description",
}
type CBAEventHandler struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Description string `json:"description"`
}
var CBAEventHandlerProperties []string = []string{
"Name",
"Enabled",
"Description",
}
type settingsJson struct {
Influx struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Token string `json:"token"`
Org string `json:"org"`
} `json:"influxdb"`
Timescale struct {
Enabled bool `json:"enabled"`
ConnectionUrl string `json:"connectionUrl"`
DatabaseName string `json:"databaseName"`
} `json:"timescaledb"`
Arma3 struct {
RefreshRateMs int `json:"refreshRateMs"`
Debug bool `json:"debug"`
} `json:"arma3"`
RecordingSettings map[string]interface{} `json:"recordingSettings"`
}
var activeSettings settingsJson
// InfluxDB variables // InfluxDB variables
var DB_CLIENT influxdb2.Client var influxClient influxdb2.Client
// TimescaleDB variables
var timescaleDbPool *pgxpool.Pool
// file paths // file paths
var ADDON_FOLDER string = "./@RangerMetrics" var ADDON_FOLDER string = getDir() + "\\@RangerMetrics"
var LOG_FILE string = ADDON_FOLDER + "/rangermetrics.log" var LOG_FILE string = ADDON_FOLDER + "\\rangermetrics.log"
var SETTINGS_FILE string = ADDON_FOLDER + "/settings.json" var SETTINGS_FILE string = ADDON_FOLDER + "\\settings.json"
var SETTINGS_FILE_EXAMPLE string = ADDON_FOLDER + "\\settings.example.json"
// var BACKUP_FILE_PATH string = ADDON_FOLDER + "/local_backup.log.gzip" // var BACKUP_FILE_PATH string = ADDON_FOLDER + "/local_backup.log.gzip"
// var BACKUP_WRITER *gzip.Writer // var BACKUP_WRITER *gzip.Writer
@@ -65,36 +124,72 @@ func init() {
log.Fatalf("error opening file: %v", err) log.Fatalf("error opening file: %v", err)
} }
// log to console as well // log to console as well
log.SetOutput(io.MultiWriter(f, os.Stdout)) // log.SetOutput(io.MultiWriter(f, os.Stdout))
// log only to file
log.SetOutput(f)
}
func initExtension() {
functionName := "initExtension"
logLine(functionName, `["Initializing RangerMetrics extension", "INFO"]`, false)
logLine(functionName, fmt.Sprintf(`["RangerMetrics Extension Version: %v", "INFO"]`, EXTENSION_VERSION), false)
var err error
activeSettings, err = loadSettings()
if err != nil {
logLine(functionName, fmt.Sprintf(`["Error loading settings: %v", "ERROR"]`, err), false)
return
}
if activeSettings.Influx.Enabled {
influxClient, err = connectToInflux()
if err != nil {
logLine(functionName, fmt.Sprintf(`["Error connecting to InfluxDB: %v", "ERROR"]`, err), false)
return
}
}
if activeSettings.Timescale.Enabled {
timescaleDbPool, err = connectToTimescale()
if err != nil {
logLine(functionName, fmt.Sprintf(`["Error connecting to TimescaleDB: %v", "ERROR"]`, err), false)
return
}
initTimescale()
}
logLine(functionName, `["Extension ready", "INFO"]`, false)
}
func deinitExtension() {
functionName := "deinitExtension"
logLine(functionName, `["Deinitializing RangerMetrics extension", "INFO"]`, false)
if timescaleDbPool != nil {
logLine(functionName, `["Closing TimescaleDB connection", "INFO"]`, false)
timescaleDbPool.Close()
} else {
logLine(functionName, `["TimescaleDB connection not open", "INFO"]`, false)
}
logLine(functionName, `[true]`, false)
} }
// func RVExtensionContext(output *C.char, argc *C.int) { // func RVExtensionContext(output *C.char, argc *C.int) {
// } // }
type influxSettings struct { func version() {
Host string `json:"host"` functionName := "version"
Token string `json:"token"` logLine(functionName, fmt.Sprintf(`["RangerMetrics Extension Version:%s", "INFO"]`, EXTENSION_VERSION), false)
Org string `json:"org"`
} }
type timescaleSettings struct { // return db client and error
ConnectionUrl string `json:"connectionUrl"` func connectToInflux() (influxdb2.Client, error) {
} if activeSettings.Influx.Host == "" ||
activeSettings.Influx.Host == "http://host:8086" {
type arma3Settings struct { return nil, errors.New("influxConnectionSettings.Host is empty")
RefreshRateMs int `json:"refreshRateMs"`
}
type settingsJson struct {
Influx influxSettings `json:"influxdb"`
Arma3 arma3Settings `json:"arma3"`
Timescale timescaleSettings `json:"timescaledb"`
}
func connectToInflux() {
if influxConnectionSettings.Host == "" {
logLine("connectToInflux", `["influxConnectionSettings.Host is empty", "ERROR"]`)
// logLine("connectToInflux", `["Creating backup file", "INFO"]`) // logLine("connectToInflux", `["Creating backup file", "INFO"]`)
// file, err := os.Open(BACKUP_FILE_PATH) // file, err := os.Open(BACKUP_FILE_PATH)
// if err != nil { // if err != nil {
@@ -109,42 +204,70 @@ func connectToInflux() {
// return "Error connecting to Influx. Using local backup" // return "Error connecting to Influx. Using local backup"
} }
DB_CLIENT = influxdb2.NewClientWithOptions(influxConnectionSettings.Host, influxConnectionSettings.Token, influxdb2.DefaultOptions().SetBatchSize(500).SetFlushInterval(2000)) influxClient := influxdb2.NewClientWithOptions(activeSettings.Influx.Host, activeSettings.Influx.Token, influxdb2.DefaultOptions().SetBatchSize(1000).SetFlushInterval(1000))
logLine("connectToInflux", `["DB_CLIENT created", "INFO"]`) return influxClient, nil
} }
////////////////////////////////// //////////////////////////////////
// TIMESCALE // TIMESCALE
////////////////////////////////// //////////////////////////////////
func connectToTimescale() { func connectToTimescale() (*pgxpool.Pool, error) {
functionName := "connectToTimescale" functionName := "connectToTimescale"
var err error var err error
// urlExample := "postgres://username:password@localhost:5432/database_name" // urlExample := "postgres://username:password@localhost:5432/database_name"
// logLine("connectToTimescale", fmt.Sprintf(`["timescaleConnectionSettings.ConnectionUrl: %s", "INFO"]`, timescaleConnectionSettings.ConnectionUrl)) // logLine("connectToTimescale", fmt.Sprintf(`["timescaleConnectionSettings.ConnectionUrl: %s", "INFO"]`, timescaleConnectionSettings.ConnectionUrl))
conn, err := pgx.Connect(context.Background(), timescaleConnectionSettings.ConnectionUrl) conn, err := pgx.Connect(context.Background(), activeSettings.Timescale.ConnectionUrl+"/postgres")
if err != nil { if err != nil {
logLine( logLine(
functionName, functionName,
fmt.Sprintf(`["Unable to connect to Timescale DB: %v", "ERROR"]`, err.Error()), fmt.Sprintf(`["Error connecting to Timescale DB: %v", "ERROR"]`, err.Error()),
false,
) )
// return fmt.Sprintf(`["Error connecting to timescale DB: %v\n"]`, err.Error()) return nil, err
} }
conn.Exec(context.Background(), "CREATE DATABASE rangermetrics;") // ensure database exists
logLine("connectToTimescale", `["Connected to Timescale successfully", "INFO"]`) logLine(
functionName,
fmt.Sprintf(`["TimescaleDB: 'CREATE DATABASE %s'", "INFO"]`, activeSettings.Timescale.DatabaseName),
false,
)
conn.Query(context.Background(), fmt.Sprintf(`CREATE DATABASE %s`, activeSettings.Timescale.DatabaseName))
// close connection
conn.Close(context.Background())
// create connection pool
var dbPool *pgxpool.Pool
dbPool, err = pgxpool.Connect(
context.Background(),
fmt.Sprintf(`%s/%s`, activeSettings.Timescale.ConnectionUrl, activeSettings.Timescale.DatabaseName),
)
if err != nil {
logLine(
functionName,
fmt.Sprintf(`["Error connecting to Timescale DB: %v", "ERROR"]`, err.Error()),
false,
)
return nil, err
}
logLine("connectToTimescale", `["Connected to Timescale successfully", "INFO"]`, false)
return dbPool, nil
} }
func initTimescale() { func initTimescale() {
// create a table
// CREATE TABLE IF NOT EXISTS rangermetrics (time timestamp, line text);
timescaleDbPool.Exec(context.Background(), "CREATE DATABASE rangermetrics;")
// create entities table functionName := "initTimescale"
timescaleDbPool.Exec(context.Background(), `CREATE TABLE "public.Missions" ( var err error
// schema init sql
var tableCreationSql string = `
CREATE TABLE "Missions" (
"id" serial NOT NULL UNIQUE, "id" serial NOT NULL UNIQUE,
"world_name" VARCHAR(255) NOT NULL, "world_name" VARCHAR(255) NOT NULL,
"briefing_name" VARCHAR(255) NOT NULL, "briefing_name" VARCHAR(255) NOT NULL,
@@ -155,8 +278,8 @@ func initTimescale() {
"ace_medical" BOOLEAN NOT NULL, "ace_medical" BOOLEAN NOT NULL,
"radio_tfar" BOOLEAN NOT NULL, "radio_tfar" BOOLEAN NOT NULL,
"radio_acre" BOOLEAN NOT NULL, "radio_acre" BOOLEAN NOT NULL,
"start_game" DATETIME NOT NULL, "start_game" TIMESTAMP NOT NULL,
"start_utc" DATETIME NOT NULL, "start_utc" TIMESTAMP NOT NULL,
"frame_count" FLOAT NOT NULL, "frame_count" FLOAT NOT NULL,
"capture_delay_s" FLOAT NOT NULL, "capture_delay_s" FLOAT NOT NULL,
"addon_ver_major" integer NOT NULL, "addon_ver_major" integer NOT NULL,
@@ -173,7 +296,7 @@ func initTimescale() {
CREATE TABLE "public.Worlds" ( CREATE TABLE "Worlds" (
"world_name" VARCHAR(255) NOT NULL, "world_name" VARCHAR(255) NOT NULL,
"display_name" VARCHAR(255) NOT NULL, "display_name" VARCHAR(255) NOT NULL,
"world_size_m" integer NOT NULL, "world_size_m" integer NOT NULL,
@@ -184,7 +307,7 @@ func initTimescale() {
CREATE TABLE "public.Units" ( CREATE TABLE "Units" (
"mission_id" integer NOT NULL, "mission_id" integer NOT NULL,
"unit_id" integer NOT NULL, "unit_id" integer NOT NULL,
"frame" integer NOT NULL, "frame" integer NOT NULL,
@@ -213,7 +336,7 @@ func initTimescale() {
CREATE TABLE "public.Vehicles" ( CREATE TABLE "Vehicles" (
"mission_id" integer NOT NULL, "mission_id" integer NOT NULL,
"frame" integer NOT NULL, "frame" integer NOT NULL,
"vehicle_id" integer NOT NULL, "vehicle_id" integer NOT NULL,
@@ -233,7 +356,7 @@ func initTimescale() {
CREATE TABLE "public.Markers" ( CREATE TABLE "Markers" (
"mission_id" integer NOT NULL, "mission_id" integer NOT NULL,
"frame" integer NOT NULL, "frame" integer NOT NULL,
"marker_name" VARCHAR(255) NOT NULL, "marker_name" VARCHAR(255) NOT NULL,
@@ -245,7 +368,7 @@ func initTimescale() {
CREATE TABLE "public.Chat" ( CREATE TABLE "Chat" (
"id" serial NOT NULL, "id" serial NOT NULL,
"mission_id" integer NOT NULL, "mission_id" integer NOT NULL,
"sender_id" integer NOT NULL, "sender_id" integer NOT NULL,
@@ -260,7 +383,7 @@ func initTimescale() {
CREATE TABLE "public.UniqueUsers" ( CREATE TABLE "UniqueUsers" (
"steamid64" varchar(100) NOT NULL, "steamid64" varchar(100) NOT NULL,
CONSTRAINT "UniqueUsers_pk" PRIMARY KEY ("steamid64") CONSTRAINT "UniqueUsers_pk" PRIMARY KEY ("steamid64")
) WITH ( ) WITH (
@@ -269,11 +392,11 @@ func initTimescale() {
CREATE TABLE "public.Environment" ( CREATE TABLE "Environment" (
"mission_id" integer NOT NULL, "mission_id" integer NOT NULL,
"frame" integer NOT NULL, "frame" integer NOT NULL,
"date_game" DATETIME NOT NULL, "date_game" TIMESTAMP NOT NULL,
"date_utc" DATETIME NOT NULL, "date_utc" TIMESTAMP NOT NULL,
"overcast" FLOAT NOT NULL, "overcast" FLOAT NOT NULL,
"rain" FLOAT NOT NULL, "rain" FLOAT NOT NULL,
"humidity" FLOAT NOT NULL, "humidity" FLOAT NOT NULL,
@@ -290,7 +413,7 @@ func initTimescale() {
CREATE TABLE "public.StaticObjects" ( CREATE TABLE "StaticObjects" (
"mission_id" integer NOT NULL, "mission_id" integer NOT NULL,
"frame" integer NOT NULL, "frame" integer NOT NULL,
"building_id" integer NOT NULL, "building_id" integer NOT NULL,
@@ -308,7 +431,7 @@ func initTimescale() {
CREATE TABLE "public.Campaigns" ( CREATE TABLE "Campaigns" (
"id" serial NOT NULL, "id" serial NOT NULL,
"name" VARCHAR(255) NOT NULL, "name" VARCHAR(255) NOT NULL,
"description" TEXT NOT NULL, "description" TEXT NOT NULL,
@@ -320,38 +443,75 @@ func initTimescale() {
CREATE TABLE "public.Missions_In_Campaigns" ( CREATE TABLE "Missions_In_Campaigns" (
"mission" integer NOT NULL, "mission" integer NOT NULL,
"campaign" integer NOT NULL, "campaign" integer NOT NULL,
CONSTRAINT "Missions_In_Campaigns_pk" PRIMARY KEY ("mission","campaign") CONSTRAINT "Missions_In_Campaigns_pk" PRIMARY KEY ("mission","campaign")
) WITH ( ) WITH (
OIDS=FALSE OIDS=FALSE
); );
`
relationCreationSql := []string{
`ALTER TABLE "Missions" ADD CONSTRAINT "Missions_fk0" FOREIGN KEY ("world_name") REFERENCES "Worlds"("world_name");`,
`ALTER TABLE "Units" ADD CONSTRAINT "Units_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`,
`ALTER TABLE "Units" ADD CONSTRAINT "Units_fk1" FOREIGN KEY ("steamid64") REFERENCES "UniqueUsers"("steamid64");`,
ALTER TABLE "Missions" ADD CONSTRAINT "Missions_fk0" FOREIGN KEY ("world_name") REFERENCES "Worlds"("world_name"); `ALTER TABLE "Vehicles" ADD CONSTRAINT "Vehicles_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`,
`ALTER TABLE "Markers" ADD CONSTRAINT "Markers_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`,
ALTER TABLE "Units" ADD CONSTRAINT "Units_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); `ALTER TABLE "Chat" ADD CONSTRAINT "Chat_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`,
ALTER TABLE "Units" ADD CONSTRAINT "Units_fk1" FOREIGN KEY ("steamid64") REFERENCES "UniqueUsers"("steamid64"); `ALTER TABLE "Chat" ADD CONSTRAINT "Chat_fk1" FOREIGN KEY ("sender_id") REFERENCES "Units"("unit_id");`,
ALTER TABLE "Vehicles" ADD CONSTRAINT "Vehicles_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); `ALTER TABLE "Environment" ADD CONSTRAINT "Environment_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`,
ALTER TABLE "Markers" ADD CONSTRAINT "Markers_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); `ALTER TABLE "StaticObjects" ADD CONSTRAINT "StaticObjects_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`,
ALTER TABLE "Chat" ADD CONSTRAINT "Chat_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); `ALTER TABLE "Missions_In_Campaigns" ADD CONSTRAINT "Missions_In_Campaigns_fk0" FOREIGN KEY ("mission") REFERENCES "Missions"("id");`,
ALTER TABLE "Chat" ADD CONSTRAINT "Chat_fk1" FOREIGN KEY ("sender_id") REFERENCES "Units"("unit_id"); `ALTER TABLE "Missions_In_Campaigns" ADD CONSTRAINT "Missions_In_Campaigns_fk1" FOREIGN KEY ("campaign") REFERENCES "Campaigns"("id");`,
}
var tx pgx.Tx
tx, err = timescaleDbPool.Begin(context.Background())
if err != nil {
logLine(functionName, fmt.Sprintf(`["Error creating transaction: %v", "ERROR"]`, err.Error()), false)
return
}
ALTER TABLE "Environment" ADD CONSTRAINT "Environment_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); _, err = tx.Exec(context.Background(), tableCreationSql)
if err != nil {
logLine(functionName, fmt.Sprintf(`["Error creating tables: %v", "ERROR"]`, err.Error()), false)
tx.Rollback(context.Background())
tx.Commit(context.Background())
return
}
err = tx.Commit(context.Background())
if err != nil {
logLine(functionName, fmt.Sprintf(`["Error committing table creation transaction: %v", "ERROR"]`, err.Error()), false)
return
}
ALTER TABLE "StaticObjects" ADD CONSTRAINT "StaticObjects_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); // run each relation creation sql statement
for _, sql := range relationCreationSql {
_, err = tx.Exec(context.Background(), sql)
if err != nil {
logLine(functionName, fmt.Sprintf(`["Error creating relation: %v", "ERROR"]`, err.Error()), false)
tx.Rollback(context.Background())
tx.Commit(context.Background())
return
}
}
ALTER TABLE "Missions_In_Campaigns" ADD CONSTRAINT "Missions_In_Campaigns_fk0" FOREIGN KEY ("mission") REFERENCES "Missions"("id"); err = tx.Commit(context.Background())
ALTER TABLE "Missions_In_Campaigns" ADD CONSTRAINT "Missions_In_Campaigns_fk1" FOREIGN KEY ("campaign") REFERENCES "Campaigns"("id"); if err != nil {
`) logLine(functionName, fmt.Sprintf(`["Error committing relation creation transaction: %v", "ERROR"]`, err.Error()), false)
return
}
logLine(functionName, `["Timescale schema initialized", "INFO"]`, false)
} }
@@ -360,14 +520,71 @@ func writeToTimescale(table string, line string) {
functionName := "writeToTimescale" functionName := "writeToTimescale"
_, err := timescaleDbPool.Exec(context.Background(), "INSERT INTO %1 (time, line) VALUES (NOW(), $2);", table, line) _, err := timescaleDbPool.Exec(context.Background(), "INSERT INTO %1 (time, line) VALUES (NOW(), $2);", table, line)
if err != nil { if err != nil {
logLine(functionName, fmt.Sprintf(`["Error writing to timescale: %v", "ERROR"]`, err.Error())) logLine(functionName, fmt.Sprintf(`["Error writing to timescale: %v", "ERROR"]`, err.Error()), false)
} }
} }
func deinitialize() { func writeToInflux(a3DataRaw *[]string) string {
logLine("deinitialize", `["deinitialize called", "INFO"]`)
DB_CLIENT.Close() // convert to string array
timescaleDbPool.Close() a3Data := *a3DataRaw
logLine("writeToInflux", fmt.Sprintf(`["Received %d params", "DEBUG"]`, len(a3Data)), true)
MIN_PARAMS_COUNT := 1
var logData string
functionName := "writeToInflux"
if len(a3Data) < MIN_PARAMS_COUNT {
logData = fmt.Sprintf(`["Not all parameters present (got %d, expected at least %d)", "ERROR"]`, len(a3Data), MIN_PARAMS_COUNT)
logLine(functionName, logData, false)
return logData
}
// use custom bucket or default
var bucket string = fixEscapeQuotes(trimQuotes(string(a3Data[0])))
// Get non-blocking write client
WRITE_API := influxClient.WriteAPI(activeSettings.Influx.Org, bucket)
if WRITE_API == nil {
logData = `["Error creating write API", "ERROR"]`
logLine(functionName, logData, false)
return logData
}
// Get errors channel
errorsCh := WRITE_API.Errors()
go func() {
for writeErr := range errorsCh {
logData = fmt.Sprintf(`["Error parsing line protocol: %s", "ERROR"]`, strings.Replace(writeErr.Error(), `"`, `'`, -1))
logLine(functionName, logData, false)
}
}()
// now we have our write client, we'll go through the rest of the receive array items in line protocol format and write them to influx
for i := 1; i < len(a3Data); i++ {
var p string = fixEscapeQuotes(trimQuotes(string(a3Data[i])))
// write the line to influx
WRITE_API.WriteRecord(p)
// TODO: Add backup writer
// // append backup line to file if BACKUP_WRITER is set
// //
// if BACKUP_WRITER != nil {
// _, err = BACKUP_WRITER.Write([]byte(p + "\n"))
// }
}
// schedule cleanup
WRITE_API.Flush()
logData = fmt.Sprintf(`["Wrote %d lines to influx", "DEBUG"]`, len(a3Data)-1)
logLine(functionName, logData, true)
return "Success"
} }
// sanitize line protocol for influx // sanitize line protocol for influx
@@ -392,11 +609,14 @@ func getDir() string {
return dir return dir
} }
func loadSettings() (dir string, result string, influxHost string, timescaleUrl string) { // return true if the program should continue
func loadSettings() (settingsJson, error) {
functionName := "loadSettings" functionName := "loadSettings"
logLine(functionName, fmt.Sprintf(`["ADDON_FOLDER: %s", "INFO"]`, ADDON_FOLDER)) logLine(functionName, fmt.Sprintf(`["ADDON_FOLDER: %s", "INFO"]`, ADDON_FOLDER), false)
logLine(functionName, fmt.Sprintf(`["LOG_FILE: %s", "INFO"]`, LOG_FILE)) logLine(functionName, fmt.Sprintf(`["LOG_FILE: %s", "INFO"]`, LOG_FILE), false)
logLine(functionName, fmt.Sprintf(`["SETTINGS_FILE: %s", "INFO"]`, SETTINGS_FILE)) logLine(functionName, fmt.Sprintf(`["SETTINGS_FILE: %s", "INFO"]`, SETTINGS_FILE), false)
settings := settingsJson{}
// print the current working directory // print the current working directory
var file *os.File var file *os.File
@@ -406,66 +626,65 @@ func loadSettings() (dir string, result string, influxHost string, timescaleUrl
// see if the file exists // see if the file exists
if _, err = os.Stat(SETTINGS_FILE); os.IsNotExist(err) { if _, err = os.Stat(SETTINGS_FILE); os.IsNotExist(err) {
// file does not exist // file does not exist
log.Println("settings.json does not exist") logLine(
// create the file functionName,
file, err = os.Create(SETTINGS_FILE) fmt.Sprintf(`["%s does not exist", "ERROR"]`, SETTINGS_FILE),
false,
)
// copy settings.json.example to settings.json
// load contents
fileContents, err := ioutil.ReadFile(SETTINGS_FILE_EXAMPLE)
if err != nil { if err != nil {
log.Fatal(err) return settings, err
} }
defer file.Close() // write contents to settings.json
// write the default settings to the file err = ioutil.WriteFile(SETTINGS_FILE, fileContents, 0644)
ifSet := influxSettings{
Host: "http://localhost:8086",
Token: "my-token",
Org: "my-org",
}
a3Set := arma3Settings{
RefreshRateMs: 1000,
}
tsSettings := timescaleSettings{
ConnectionUrl: "postgres://username:password@localhost:5432/database_name",
}
defaultSettings := map[string]interface{}{
"influxdb": ifSet,
"arma3": a3Set,
"timescale": tsSettings,
}
encoder := json.NewEncoder(file)
err = encoder.Encode(defaultSettings)
if err != nil { if err != nil {
log.Fatal(err) return settings, err
} }
result = `["settings.json created - please modify!", "WARN"]`
influxHost = ifSet.Host // Exit false to discontinue initialization since settings are defaults
timescaleUrl = tsSettings.ConnectionUrl logLine(functionName, `["CREATED SETTINGS"]`, false)
return dir, result, influxHost, timescaleUrl // return a new error
return settings, errors.New("settings.json does not exist")
} else { } else {
// file exists // file exists
log.Println("settings.json exists") logLine(functionName, `["settings.json found", "DEBUG"]`, true)
// read the file // read the file
file, err = os.Open(SETTINGS_FILE) file, err = os.Open(SETTINGS_FILE)
if err != nil { if err != nil {
log.Fatal(err) return settings, err
} }
defer file.Close() defer file.Close()
decoder := json.NewDecoder(file) decoder := json.NewDecoder(file)
var settings settingsJson
err = decoder.Decode(&settings) err = decoder.Decode(&settings)
if err != nil { if err != nil {
log.Fatal(err) return settings, err
}
// set the settings
influxConnectionSettings = settings.Influx
a3Settings = settings.Arma3
timescaleConnectionSettings = settings.Timescale
// set the result
result = `["settings.json loaded", "INFO"]`
influxHost = influxConnectionSettings.Host
timescaleUrl = timescaleConnectionSettings.ConnectionUrl
} }
return dir, result, influxHost, timescaleUrl // send contents of settings file
// get the file contents
fileContents, err := ioutil.ReadFile(SETTINGS_FILE)
if err != nil {
return settings, err
}
// compact the json
var jsonStr bytes.Buffer
err = json.Compact(&jsonStr, fileContents)
if err != nil {
return settings, err
}
logLine(
"loadSettingsJSON",
jsonStr.String(),
false,
)
}
return settings, nil
} }
func runExtensionCallback(name *C.char, function *C.char, data *C.char) C.int { func runExtensionCallback(name *C.char, function *C.char, data *C.char) C.int {
@@ -542,70 +761,7 @@ func fixEscapeQuotes(s string) string {
return strings.Replace(s, `""`, `"`, -1) return strings.Replace(s, `""`, `"`, -1)
} }
func writeToInflux(a3DataRaw *[]string) string { func logLine(functionName string, data string, isDebug bool) {
// convert to string array
a3Data := *a3DataRaw
logLine("writeToInflux", fmt.Sprintf(`["Received %d params", "DEBUG"]`, len(a3Data)))
MIN_PARAMS_COUNT := 1
var logData string
functionName := "writeToInflux"
if len(a3Data) < MIN_PARAMS_COUNT {
logData = fmt.Sprintf(`["Not all parameters present (got %d, expected at least %d)", "ERROR"]`, len(a3Data), MIN_PARAMS_COUNT)
logLine(functionName, logData)
return logData
}
// use custom bucket or default
var bucket string = fixEscapeQuotes(trimQuotes(string(a3Data[0])))
// Get non-blocking write client
WRITE_API := DB_CLIENT.WriteAPI(influxConnectionSettings.Org, bucket)
if WRITE_API == nil {
logData = `["Error creating write API", "ERROR"]`
logLine(functionName, logData)
return logData
}
// Get errors channel
errorsCh := WRITE_API.Errors()
go func() {
for writeErr := range errorsCh {
logData = fmt.Sprintf(`["Error parsing line protocol: %s", "ERROR"]`, strings.Replace(writeErr.Error(), `"`, `'`, -1))
logLine(functionName, logData)
}
}()
// now we have our write client, we'll go through the rest of the receive array items in line protocol format and write them to influx
for i := 1; i < len(a3Data); i++ {
var p string = fixEscapeQuotes(trimQuotes(string(a3Data[i])))
// write the line to influx
WRITE_API.WriteRecord(p)
// TODO: Add backup writer
// // append backup line to file if BACKUP_WRITER is set
// //
// if BACKUP_WRITER != nil {
// _, err = BACKUP_WRITER.Write([]byte(p + "\n"))
// }
}
// schedule cleanup
WRITE_API.Flush()
logData = fmt.Sprintf(`["Wrote %d lines to influx", "INFO"]`, len(a3Data)-1)
logLine(functionName, logData)
return "Success"
}
func logLine(functionName string, data string) {
statusName := C.CString("RangerMetrics") statusName := C.CString("RangerMetrics")
defer C.free(unsafe.Pointer(statusName)) defer C.free(unsafe.Pointer(statusName))
statusFunction := C.CString(functionName) statusFunction := C.CString(functionName)
@@ -614,7 +770,11 @@ func logLine(functionName string, data string) {
defer C.free(unsafe.Pointer(statusParam)) defer C.free(unsafe.Pointer(statusParam))
runExtensionCallback(statusName, statusFunction, statusParam) runExtensionCallback(statusName, statusFunction, statusParam)
if activeSettings.Arma3.Debug && isDebug {
log.Println(data) log.Println(data)
} else if !isDebug {
log.Println(data)
}
} }
//export goRVExtension //export goRVExtension
@@ -622,14 +782,28 @@ func goRVExtension(output *C.char, outputsize C.size_t, input *C.char) {
var temp string var temp string
// logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "DEBUG"]`, C.GoString(input)), true)
switch C.GoString(input) {
case "version":
temp = EXTENSION_VERSION
case "getDir":
temp = getDir()
case "getUnixTimeNano":
time := getUnixTimeNano()
temp = fmt.Sprintf(`["%s"]`, strconv.FormatInt(time, 10))
default:
// check if input is in AVAILABLE_FUNCTIONS // check if input is in AVAILABLE_FUNCTIONS
// if not, return error // if not, return error
// if yes, continue // if yes, continue
if _, ok := AVAILABLE_FUNCTIONS[C.GoString(input)]; !ok { if _, ok := AVAILABLE_FUNCTIONS[C.GoString(input)]; !ok {
temp = fmt.Sprintf("Function: %s not found!", C.GoString(input)) temp = fmt.Sprintf(`["Function: %s not found!", "ERROR"]`, C.GoString(input))
} else { } else {
// call the function by name // call the function by name
reflect.ValueOf(AVAILABLE_FUNCTIONS[C.GoString(input)]).Call([]reflect.Value{}) go reflect.ValueOf(AVAILABLE_FUNCTIONS[C.GoString(input)]).Call([]reflect.Value{})
temp = fmt.Sprintf(`["Function: %s called successfully", "DEBUG"]`, C.GoString(input))
}
} }
// switch C.GoString(input) { // switch C.GoString(input) {

View File

@@ -1,11 +1,26 @@
freeExtension "RangerMetrics"; freeExtension "RangerMetrics";
// sleep 0.5; // sleep 0.5;
"RangerMetrics" callExtension "loadSettings"; // "RangerMetrics" callExtension "loadSettings";
"RangerMetrics" callExtension "version"; // "RangerMetrics" callExtension "version";
"RangerMetrics" callExtension "connectToInflux"; // "RangerMetrics" callExtension "connectToInflux";
"RangerMetrics" callExtension "connectToTimescale"; // "RangerMetrics" callExtension "connectToTimescale";
"RangerMetrics" callExtension "initTimescale"; // sleep 5;
// "RangerMetrics" callExtension "initTimescale";
sleep 30; // addMissionEventHandler ["ExtensionCallback", {
// params ["_extension", "_function", "_data"];
// if (
// _extension == "RangerMetrics" && _function isEqualTo "connectToTimescale"
// ) then {
// diag_log format ["RangerMetrics: %1", _data];
// };
// }];
"RangerMetrics" callExtension "deinitExtension";
sleep 1;
"RangerMetrics" callExtension "initExtension";
sleep 20;
"RangerMetrics" callExtension "deinitExtension";
// freeExtension "RangerMetrics"; // freeExtension "RangerMetrics";
sleep 5;
exit; exit;