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

596
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,
@@ -167,24 +290,24 @@ func initTimescale() {
"extension_ver_patch" integer NOT NULL, "extension_ver_patch" integer NOT NULL,
"tags" VARCHAR(255) NOT NULL, "tags" VARCHAR(255) NOT NULL,
CONSTRAINT "Missions_pk" PRIMARY KEY ("id") CONSTRAINT "Missions_pk" PRIMARY KEY ("id")
) WITH ( ) WITH (
OIDS=FALSE OIDS=FALSE
); );
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,
CONSTRAINT "Worlds_pk" PRIMARY KEY ("world_name") CONSTRAINT "Worlds_pk" PRIMARY KEY ("world_name")
) WITH ( ) WITH (
OIDS=FALSE OIDS=FALSE
); );
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,
@@ -207,13 +330,13 @@ func initTimescale() {
"damage" FLOAT NOT NULL, "damage" FLOAT NOT NULL,
"is_speaking" integer NOT NULL, "is_speaking" integer NOT NULL,
CONSTRAINT "Units_pk" PRIMARY KEY ("mission_id","unit_id","frame") CONSTRAINT "Units_pk" PRIMARY KEY ("mission_id","unit_id","frame")
) WITH ( ) WITH (
OIDS=FALSE OIDS=FALSE
); );
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,
@@ -227,25 +350,25 @@ func initTimescale() {
"is_alive" BOOLEAN NOT NULL, "is_alive" BOOLEAN NOT NULL,
"damage" FLOAT NOT NULL, "damage" FLOAT NOT NULL,
CONSTRAINT "Vehicles_pk" PRIMARY KEY ("mission_id","frame","vehicle_id") CONSTRAINT "Vehicles_pk" PRIMARY KEY ("mission_id","frame","vehicle_id")
) WITH ( ) WITH (
OIDS=FALSE OIDS=FALSE
); );
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,
"data" VARCHAR(255) NOT NULL, "data" VARCHAR(255) NOT NULL,
CONSTRAINT "Markers_pk" PRIMARY KEY ("mission_id","frame","marker_name") CONSTRAINT "Markers_pk" PRIMARY KEY ("mission_id","frame","marker_name")
) WITH ( ) WITH (
OIDS=FALSE OIDS=FALSE
); );
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,
@@ -254,26 +377,26 @@ func initTimescale() {
"content" VARCHAR(255) NOT NULL, "content" VARCHAR(255) NOT NULL,
"channel" integer NOT NULL, "channel" integer NOT NULL,
CONSTRAINT "Chat_pk" PRIMARY KEY ("id") CONSTRAINT "Chat_pk" PRIMARY KEY ("id")
) WITH ( ) WITH (
OIDS=FALSE OIDS=FALSE
); );
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 (
OIDS=FALSE OIDS=FALSE
); );
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,
@@ -284,13 +407,13 @@ func initTimescale() {
"gusts" FLOAT NOT NULL, "gusts" FLOAT NOT NULL,
"waves" FLOAT NOT NULL, "waves" FLOAT NOT NULL,
CONSTRAINT "Environment_pk" PRIMARY KEY ("mission_id","frame") CONSTRAINT "Environment_pk" PRIMARY KEY ("mission_id","frame")
) WITH ( ) WITH (
OIDS=FALSE OIDS=FALSE
); );
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,
@@ -302,56 +425,93 @@ func initTimescale() {
"classname" VARCHAR(255) NOT NULL, "classname" VARCHAR(255) NOT NULL,
"simple_object_data" varchar(1500) NOT NULL, "simple_object_data" varchar(1500) NOT NULL,
CONSTRAINT "StaticObjects_pk" PRIMARY KEY ("mission_id","frame","building_id") CONSTRAINT "StaticObjects_pk" PRIMARY KEY ("mission_id","frame","building_id")
) WITH ( ) WITH (
OIDS=FALSE OIDS=FALSE
); );
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,
"image" bytea NOT NULL, "image" bytea NOT NULL,
CONSTRAINT "Campaigns_pk" PRIMARY KEY ("id") CONSTRAINT "Campaigns_pk" PRIMARY KEY ("id")
) WITH ( ) WITH (
OIDS=FALSE OIDS=FALSE
); );
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;