diff --git a/Companion/exporter/exporter.go b/Companion/exporter/exporter.go index df3131b..2bdd2e5 100644 --- a/Companion/exporter/exporter.go +++ b/Companion/exporter/exporter.go @@ -43,7 +43,32 @@ func NewPrometheusExporter(frmApiHosts []string) *PrometheusExporter { portalCollector := NewPortalCollector("/getPortal") hypertubeCollector := NewHypertubeCollector("/getHyperEntrance") frackingCollector := NewFrackingCollector("/getFrackingActivator") - collectorRunners = append(collectorRunners, NewCollectorRunner(ctx, frmApiHost, productionCollector, powerCollector, buildingCollector, vehicleCollector, trainCollector, droneCollector, vehicleStationCollector, trainStationCollector, resourceSinkCollector, pumpCollector, extractorCollector, portalCollector, hypertubeCollector, frackingCollector)) + cloudInventoryCollector := NewCloudInventoryCollector("/getCloudInv") + worldInventoryCollector := NewWorldInventoryCollector("/getWorldInv") + storageInventoryCollector := NewStorageInventoryCollector("/getStorageInv") + crateInventoryCollector := NewCrateInventoryCollector("/getCrateInv") + collectorRunners = append(collectorRunners, NewCollectorRunner( + ctx, + frmApiHost, + productionCollector, + powerCollector, + buildingCollector, + vehicleCollector, + trainCollector, + droneCollector, + vehicleStationCollector, + trainStationCollector, + resourceSinkCollector, + pumpCollector, + extractorCollector, + portalCollector, + hypertubeCollector, + frackingCollector, + cloudInventoryCollector, + storageInventoryCollector, + crateInventoryCollector, + worldInventoryCollector, + )) } return &PrometheusExporter{ diff --git a/Companion/exporter/factory_build_detail.go b/Companion/exporter/factory_build_detail.go index 1d167ea..f582309 100644 --- a/Companion/exporter/factory_build_detail.go +++ b/Companion/exporter/factory_build_detail.go @@ -1,18 +1,21 @@ package exporter type BuildingDetail struct { - Building string `json:"Name"` - Location Location `json:"location"` - Recipe string `json:"Recipe"` - Production []Production `json:"production"` - Ingredients []Ingredient `json:"ingredients"` - ManuSpeed float64 `json:"ManuSpeed"` - IsConfigured bool `json:"IsConfigured"` - IsProducing bool `json:"IsProducing"` - IsPaused bool `json:"IsPaused"` - CircuitGroupId int `json:"CircuitGroupID"` - PowerInfo PowerInfo `json:"PowerInfo"` - Somersloops float64 `json:"Somersloops"` + Id string `json:"ID"` + Building string `json:"Name"` + Location Location `json:"location"` + Recipe string `json:"Recipe"` + Production []Production `json:"production"` + Ingredients []Ingredient `json:"ingredients"` + ManuSpeed float64 `json:"ManuSpeed"` + IsConfigured bool `json:"IsConfigured"` + IsProducing bool `json:"IsProducing"` + IsPaused bool `json:"IsPaused"` + CircuitGroupId int `json:"CircuitGroupID"` + PowerInfo PowerInfo `json:"PowerInfo"` + Somersloops float64 `json:"Somersloops"` + InputInventory []InventoryItem `json:"InputInventory"` + OutputInventory []InventoryItem `json:"OutputInventory"` } type Production struct { diff --git a/Companion/exporter/factory_building_collector.go b/Companion/exporter/factory_building_collector.go index 1222dc9..6cfabba 100644 --- a/Companion/exporter/factory_building_collector.go +++ b/Companion/exporter/factory_building_collector.go @@ -34,11 +34,7 @@ func (c *FactoryBuildingCollector) Collect(frmAddress string, sessionName string powerInfo := map[float64]float64{} maxPowerInfo := map[float64]float64{} for _, building := range details { - c.metricsDropper.CacheFreshMetricLabel(prometheus.Labels{"url": frmAddress, "session_name": sessionName, "machine_name": building.Building, - "x": strconv.FormatFloat(building.Location.X, 'f', -1, 64), - "y": strconv.FormatFloat(building.Location.Y, 'f', -1, 64), - "z": strconv.FormatFloat(building.Location.Z, 'f', -1, 64), - }) + c.metricsDropper.CacheFreshMetricLabel(prometheus.Labels{"url": frmAddress, "session_name": sessionName, "id": building.Id}) for _, prod := range building.Production { MachineItemsProducedPerMin.WithLabelValues( prod.Name, @@ -57,6 +53,53 @@ func (c *FactoryBuildingCollector) Collect(frmAddress string, sessionName string strconv.FormatFloat(building.Location.Z, 'f', -1, 64), frmAddress, sessionName, ).Set(prod.ProdPercent) + + MachineItemsProducedMax.WithLabelValues( + prod.Name, + building.Building, + strconv.FormatFloat(building.Location.X, 'f', -1, 64), + strconv.FormatFloat(building.Location.Y, 'f', -1, 64), + strconv.FormatFloat(building.Location.Z, 'f', -1, 64), + frmAddress, sessionName, + ).Set(prod.MaxProd) + } + + for _, item := range building.InputInventory { + MachineInputInventory.WithLabelValues( + item.Name, + building.Building, + strconv.FormatFloat(building.Location.X, 'f', -1, 64), + strconv.FormatFloat(building.Location.Y, 'f', -1, 64), + strconv.FormatFloat(building.Location.Z, 'f', -1, 64), + frmAddress, sessionName, + ).Set(float64(item.Amount)) + MachineInputInventoryMax.WithLabelValues( + item.Name, + building.Building, + strconv.FormatFloat(building.Location.X, 'f', -1, 64), + strconv.FormatFloat(building.Location.Y, 'f', -1, 64), + strconv.FormatFloat(building.Location.Z, 'f', -1, 64), + frmAddress, sessionName, + ).Set(float64(item.MaxAmount)) + } + + for _, item := range building.OutputInventory { + MachineOutputInventory.WithLabelValues( + item.Name, + building.Building, + strconv.FormatFloat(building.Location.X, 'f', -1, 64), + strconv.FormatFloat(building.Location.Y, 'f', -1, 64), + strconv.FormatFloat(building.Location.Z, 'f', -1, 64), + frmAddress, sessionName, + ).Set(float64(item.Amount)) + MachineOutputInventoryMax.WithLabelValues( + item.Name, + building.Building, + strconv.FormatFloat(building.Location.X, 'f', -1, 64), + strconv.FormatFloat(building.Location.Y, 'f', -1, 64), + strconv.FormatFloat(building.Location.Z, 'f', -1, 64), + frmAddress, sessionName, + ).Set(float64(item.MaxAmount)) } val, ok := powerInfo[building.PowerInfo.CircuitGroupId] diff --git a/Companion/exporter/factory_building_collector_test.go b/Companion/exporter/factory_building_collector_test.go index 88d910f..41f9a39 100644 --- a/Companion/exporter/factory_building_collector_test.go +++ b/Companion/exporter/factory_building_collector_test.go @@ -61,6 +61,30 @@ var _ = Describe("FactoryBuildingCollector", func() { PowerConsumed: 23, MaxPowerConsumed: 4, }, + InputInventory: []exporter.InventoryItem{ + { + Name: "Iron Ore", + Amount: 64, + MaxAmount: 100, + }, + { + Name: "Second input", + Amount: 32, + MaxAmount: 1000, + }, + }, + OutputInventory: []exporter.InventoryItem{ + { + Name: "Iron Ingot", + Amount: 33, + MaxAmount: 200, + }, + { + Name: "Second output", + Amount: 44, + MaxAmount: 2000, + }, + }, }, }) }) @@ -112,6 +136,161 @@ var _ = Describe("FactoryBuildingCollector", func() { }) }) + Describe("Machine item max production metrics", func() { + It("records a metric with labels for the produced item name, machine type, and x, y, z coordinates", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.MachineItemsProducedMax, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current max production as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.MachineItemsProducedMax, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(float64(10))) + }) + + Describe("when a machine has multiple outputs", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ironIngots, err := gaugeValue(exporter.MachineItemsProducedMax, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironIngots).To(Equal(float64(10.0))) + + ironNothing, err := gaugeValue(exporter.MachineItemsProducedMax, "Iron Nothing", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironNothing).To(Equal(float64(4000.0))) + }) + }) + }) + + Describe("Machine input inventory metrics", func() { + It("records a metric with labels for the stored item name, machine type, and x, y, z coordinates", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.MachineInputInventory, "Iron Ore", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current input invetory as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.MachineInputInventory, "Iron Ore", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(float64(64.0))) + }) + + Describe("when a machine has multiple inputs", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ironIngots, err := gaugeValue(exporter.MachineInputInventory, "Iron Ore", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironIngots).To(Equal(float64(64.0))) + + ironNothing, err := gaugeValue(exporter.MachineInputInventory, "Second input", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironNothing).To(Equal(float64(32.0))) + }) + }) + }) + + Describe("Machine input inventory max metrics", func() { + It("records a metric with labels for the stored item name, machine type, and x, y, z coordinates", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.MachineInputInventoryMax, "Iron Ore", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current input invetory max as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.MachineInputInventoryMax, "Iron Ore", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(float64(100.0))) + }) + + Describe("when a machine has multiple inputs", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ironIngots, err := gaugeValue(exporter.MachineInputInventoryMax, "Iron Ore", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironIngots).To(Equal(float64(100.0))) + + ironNothing, err := gaugeValue(exporter.MachineInputInventoryMax, "Second input", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironNothing).To(Equal(float64(1000.0))) + }) + }) + }) + + Describe("Machine input inventory metrics", func() { + It("records a metric with labels for the stored item name, machine type, and x, y, z coordinates", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.MachineInputInventory, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current output invetory as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.MachineOutputInventory, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(float64(33.0))) + }) + + Describe("when a machine has multiple outputs", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ironIngots, err := gaugeValue(exporter.MachineOutputInventory, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironIngots).To(Equal(float64(33.0))) + + ironNothing, err := gaugeValue(exporter.MachineOutputInventory, "Second output", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironNothing).To(Equal(float64(44.0))) + }) + }) + }) + + Describe("Machine output inventory max metrics", func() { + It("records a metric with labels for the stored item name, machine type, and x, y, z coordinates", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.MachineOutputInventoryMax, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current output invetory max as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.MachineOutputInventoryMax, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(float64(200.0))) + }) + + Describe("when a machine has multiple outputs", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ironIngots, err := gaugeValue(exporter.MachineOutputInventoryMax, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironIngots).To(Equal(float64(200.0))) + + ironNothing, err := gaugeValue(exporter.MachineOutputInventoryMax, "Second output", "Smelter", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironNothing).To(Equal(float64(2000.0))) + }) + }) + }) + Describe("Machine item production efficiency metrics", func() { It("records a metric with labels for the produced item name, machine type, and x, y, z coordinates", func() { collector.Collect(url, sessionName) diff --git a/Companion/exporter/factory_building_metrics.go b/Companion/exporter/factory_building_metrics.go index d8a9d59..41ac7c8 100644 --- a/Companion/exporter/factory_building_metrics.go +++ b/Companion/exporter/factory_building_metrics.go @@ -26,6 +26,62 @@ var ( "y", "z", }) + + MachineItemsProducedMax = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "machine_items_produced_max", + Help: "The maximum of a certain item which the machine can produce", + }, []string{ + "item_name", + "machine_name", + "x", + "y", + "z", + }) + + MachineInputInventory = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "machine_input_inventory", + Help: "How much of an item a building has stored in its input", + }, []string{ + "item_name", + "machine_name", + "x", + "y", + "z", + }) + + MachineInputInventoryMax = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "machine_input_inventory_max", + Help: "How much of an item a building can store in its input", + }, []string{ + "item_name", + "machine_name", + "x", + "y", + "z", + }) + + MachineOutputInventory = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "machine_output_inventory", + Help: "How much of an item a building has stored in its output", + }, []string{ + "item_name", + "machine_name", + "x", + "y", + "z", + }) + + MachineOutputInventoryMax = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "machine_output_inventory_max", + Help: "How much of an item a building can store in its output", + }, []string{ + "item_name", + "machine_name", + "x", + "y", + "z", + }) + FactoryPower = RegisterNewGaugeVec(prometheus.GaugeOpts{ Name: "factory_power", Help: "Power draw from factory machines in MW. Does not include extractors.", diff --git a/Companion/exporter/frm_server_fake_test.go b/Companion/exporter/frm_server_fake_test.go index 3090175..9cda75a 100644 --- a/Companion/exporter/frm_server_fake_test.go +++ b/Companion/exporter/frm_server_fake_test.go @@ -27,6 +27,10 @@ type FRMServerFake struct { portalData []exporter.PortalDetails hypertubeData []exporter.HypertubeDetails frackingData []exporter.FrackingDetails + cloudInventoryData []exporter.InventoryItem + worldInventoryData []exporter.InventoryItem + storageContainerData []exporter.ContainerDetail + crateData []exporter.ContainerDetail } func NewFRMServerFake() *FRMServerFake { @@ -54,6 +58,10 @@ func NewFRMServerFake() *FRMServerFake { mux.Handle("/getPortal", http.HandlerFunc(getStatsHandler(&fake.portalData))) mux.Handle("/getHyperEntrance", http.HandlerFunc(getStatsHandler(&fake.hypertubeData))) mux.Handle("/getFrackingActivator", http.HandlerFunc(getStatsHandler(&fake.frackingData))) + mux.Handle("/getCloudInventory", http.HandlerFunc(getStatsHandler(&fake.cloudInventoryData))) + mux.Handle("/getWorldInventory", http.HandlerFunc(getStatsHandler(&fake.worldInventoryData))) + mux.Handle("/getStorage", http.HandlerFunc(getStatsHandler(&fake.storageContainerData))) + mux.Handle("/getCrates", http.HandlerFunc(getStatsHandler(&fake.crateData))) return fake } @@ -140,6 +148,22 @@ func (e *FRMServerFake) ReturnsSessionInfoData(data exporter.SessionInfo) { e.sessionInfoData = data } +func (e *FRMServerFake) ReturnsCloudInventoryData(data []exporter.InventoryItem) { + e.cloudInventoryData = data +} + +func (e *FRMServerFake) ReturnsWorldInventoryData(data []exporter.InventoryItem) { + e.worldInventoryData = data +} + +func (e *FRMServerFake) ReturnsStorageContainerData(data []exporter.ContainerDetail) { + e.storageContainerData = data +} + +func (e *FRMServerFake) ReturnsCrateData(data []exporter.ContainerDetail) { + e.crateData = data +} + func getStatsHandler(data any) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { jsonBytes, err := json.Marshal(data) diff --git a/Companion/exporter/inventory_collector_cloud.go b/Companion/exporter/inventory_collector_cloud.go new file mode 100644 index 0000000..2049aad --- /dev/null +++ b/Companion/exporter/inventory_collector_cloud.go @@ -0,0 +1,31 @@ +package exporter + +import ( + "log" +) + +type CloudInventoryCollector struct { + endpoint string +} + +func NewCloudInventoryCollector(endpoint string) *CloudInventoryCollector { + return &CloudInventoryCollector{ + endpoint: endpoint, + } +} + +func (c *CloudInventoryCollector) Collect(frmAddress string, sessionName string) { + items := []InventoryItem{} + err := retrieveData(frmAddress, c.endpoint, &items) + if err != nil { + log.Printf("error reading inventory statistics from FRM: %s\n", err) + return + } + + for _, item := range items { + CloudInventory.WithLabelValues(item.Name, frmAddress, sessionName).Set(float64(item.Amount)) + CloudInventoryMax.WithLabelValues(item.Name, frmAddress, sessionName).Set(float64(item.MaxAmount)) + } +} + +func (c *CloudInventoryCollector) DropCache() {} diff --git a/Companion/exporter/inventory_collector_crate.go b/Companion/exporter/inventory_collector_crate.go new file mode 100644 index 0000000..2bdb547 --- /dev/null +++ b/Companion/exporter/inventory_collector_crate.go @@ -0,0 +1,65 @@ +package exporter + +import ( + "log" + "strconv" + + "github.com/prometheus/client_golang/prometheus" +) + +type CrateInventoryCollector struct { + endpoint string + metricsDropper *MetricsDropper +} + +func NewCrateInventoryCollector(endpoint string) *CrateInventoryCollector { + return &CrateInventoryCollector{ + endpoint: endpoint, + metricsDropper: NewMetricsDropper( + StorageInventory, + StorageInventoryMax, + ), + } +} + +func (c *CrateInventoryCollector) Collect(frmAddress string, sessionName string) { + details := []ContainerDetail{} + err := retrieveData(frmAddress, c.endpoint, &details) + if err != nil { + c.metricsDropper.DropStaleMetricLabels() + log.Printf("error reading inventory statistics from FRM: %s\n", err) + return + } + + for _, detail := range details { + c.metricsDropper.CacheFreshMetricLabel(prometheus.Labels{ + "url": frmAddress, + "session_name": sessionName, + "id": detail.Id, + }) + for _, item := range detail.Inventory { + CrateInventory.WithLabelValues( + item.Name, + detail.Name, + strconv.FormatFloat(detail.Location.X, 'f', -1, 64), + strconv.FormatFloat(detail.Location.Y, 'f', -1, 64), + strconv.FormatFloat(detail.Location.Z, 'f', -1, 64), + frmAddress, + sessionName, + ).Set(float64(item.Amount)) + + CrateInventoryMax.WithLabelValues( + item.Name, + detail.Name, + strconv.FormatFloat(detail.Location.X, 'f', -1, 64), + strconv.FormatFloat(detail.Location.Y, 'f', -1, 64), + strconv.FormatFloat(detail.Location.Z, 'f', -1, 64), + frmAddress, + sessionName, + ).Set(float64(item.MaxAmount)) + } + } + c.metricsDropper.DropStaleMetricLabels() +} + +func (c *CrateInventoryCollector) DropCache() {} diff --git a/Companion/exporter/inventory_collector_storage.go b/Companion/exporter/inventory_collector_storage.go new file mode 100644 index 0000000..6db732f --- /dev/null +++ b/Companion/exporter/inventory_collector_storage.go @@ -0,0 +1,65 @@ +package exporter + +import ( + "log" + "strconv" + + "github.com/prometheus/client_golang/prometheus" +) + +type StorageInventoryCollector struct { + endpoint string + metricsDropper *MetricsDropper +} + +func NewStorageInventoryCollector(endpoint string) *StorageInventoryCollector { + return &StorageInventoryCollector{ + endpoint: endpoint, + metricsDropper: NewMetricsDropper( + StorageInventory, + StorageInventoryMax, + ), + } +} + +func (c *StorageInventoryCollector) Collect(frmAddress string, sessionName string) { + details := []ContainerDetail{} + err := retrieveData(frmAddress, c.endpoint, &details) + if err != nil { + c.metricsDropper.DropStaleMetricLabels() + log.Printf("error reading inventory statistics from FRM: %s\n", err) + return + } + + for _, detail := range details { + c.metricsDropper.CacheFreshMetricLabel(prometheus.Labels{ + "url": frmAddress, + "session_name": sessionName, + "id": detail.Id, + }) + for _, item := range detail.Inventory { + StorageInventory.WithLabelValues( + item.Name, + detail.Name, + strconv.FormatFloat(detail.Location.X, 'f', -1, 64), + strconv.FormatFloat(detail.Location.Y, 'f', -1, 64), + strconv.FormatFloat(detail.Location.Z, 'f', -1, 64), + frmAddress, + sessionName, + ).Set(float64(item.Amount)) + + StorageInventoryMax.WithLabelValues( + item.Name, + detail.Name, + strconv.FormatFloat(detail.Location.X, 'f', -1, 64), + strconv.FormatFloat(detail.Location.Y, 'f', -1, 64), + strconv.FormatFloat(detail.Location.Z, 'f', -1, 64), + frmAddress, + sessionName, + ).Set(float64(item.MaxAmount)) + } + } + c.metricsDropper.DropStaleMetricLabels() +} + +func (c *StorageInventoryCollector) DropCache() {} diff --git a/Companion/exporter/inventory_collector_test.go b/Companion/exporter/inventory_collector_test.go new file mode 100644 index 0000000..e668639 --- /dev/null +++ b/Companion/exporter/inventory_collector_test.go @@ -0,0 +1,400 @@ +package exporter_test + +import ( + "github.com/AP-Hunt/FicsitRemoteMonitoringCompanion/Companion/exporter" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("InventoryCollectors", func() { + var url string + var sessionName = "default" + + BeforeEach(func() { + FRMServer.Reset() + url = FRMServer.server.URL + }) + + Describe("CloudInventoryCollector", func() { + var collector *exporter.CloudInventoryCollector + + BeforeEach(func() { + collector = exporter.NewCloudInventoryCollector("/getCloudInventory") + }) + + AfterEach(func() { + collector = nil + }) + + Describe("Cloud inventory metrics", func() { + BeforeEach(func() { + FRMServer.ReturnsCloudInventoryData([]exporter.InventoryItem{ + { + Name: "Iron Ingot", + Amount: 500, + MaxAmount: 1000, + }, + { + Name: "Copper Ingot", + Amount: 250, + MaxAmount: 500, + }, + }) + }) + + It("records metrics with labels for item name", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.CloudInventory, "Iron Ingot", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current amount as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.CloudInventory, "Iron Ingot", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(500.0)) + }) + + It("records the max amount metric", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.CloudInventoryMax, "Iron Ingot", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(1000.0)) + }) + + Describe("when there are multiple items", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ironVal, err := gaugeValue(exporter.CloudInventory, "Iron Ingot", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironVal).To(Equal(500.0)) + + copperVal, err := gaugeValue(exporter.CloudInventory, "Copper Ingot", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(copperVal).To(Equal(250.0)) + }) + }) + }) + }) + + Describe("WorldInventoryCollector", func() { + var collector *exporter.WorldInventoryCollector + + BeforeEach(func() { + collector = exporter.NewWorldInventoryCollector("/getWorldInventory") + }) + + AfterEach(func() { + collector = nil + }) + + Describe("World inventory metrics", func() { + BeforeEach(func() { + FRMServer.ReturnsWorldInventoryData([]exporter.InventoryItem{ + { + Name: "Concrete", + Amount: 10000, + MaxAmount: 50000, + }, + { + Name: "Steel Beam", + Amount: 2500, + MaxAmount: 10000, + }, + }) + }) + + It("records metrics with labels for item name", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.WorldInventory, "Concrete", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current amount as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.WorldInventory, "Concrete", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(10000.0)) + }) + + It("records the max amount metric", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.WorldInventoryMax, "Concrete", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(50000.0)) + }) + + Describe("when there are multiple items", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + concreteVal, err := gaugeValue(exporter.WorldInventory, "Concrete", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(concreteVal).To(Equal(10000.0)) + + steelVal, err := gaugeValue(exporter.WorldInventory, "Steel Beam", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(steelVal).To(Equal(2500.0)) + }) + }) + }) + }) + + Describe("StorageInventoryCollector", func() { + var collector *exporter.StorageInventoryCollector + + BeforeEach(func() { + collector = exporter.NewStorageInventoryCollector("/getStorage") + }) + + AfterEach(func() { + collector = nil + }) + + Describe("Storage inventory metrics", func() { + BeforeEach(func() { + FRMServer.ReturnsStorageContainerData([]exporter.ContainerDetail{ + { + Name: "Storage Container", + Location: exporter.Location{ + X: 150.0, + Y: 250.0, + Z: -350.0, + }, + Inventory: []exporter.InventoryItem{ + { + Name: "Iron Ore", + Amount: 1200, + MaxAmount: 2400, + }, + { + Name: "Copper Ore", + Amount: 600, + MaxAmount: 2400, + }, + }, + }, + }) + }) + + It("records metrics with labels for item name, container name, and coordinates", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.StorageInventory, "Iron Ore", "Storage Container", "150", "250", "-350", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current amount as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.StorageInventory, "Iron Ore", "Storage Container", "150", "250", "-350", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(1200.0)) + }) + + It("records the max amount metric", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.StorageInventoryMax, "Iron Ore", "Storage Container", "150", "250", "-350", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(2400.0)) + }) + + Describe("when a container has multiple items", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ironVal, err := gaugeValue(exporter.StorageInventory, "Iron Ore", "Storage Container", "150", "250", "-350", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironVal).To(Equal(1200.0)) + + copperVal, err := gaugeValue(exporter.StorageInventory, "Copper Ore", "Storage Container", "150", "250", "-350", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(copperVal).To(Equal(600.0)) + }) + }) + + Describe("when there are multiple containers", func() { + BeforeEach(func() { + FRMServer.ReturnsStorageContainerData([]exporter.ContainerDetail{ + { + Name: "Storage Container", + Location: exporter.Location{ + X: 100.0, + Y: 200.0, + Z: -300.0, + }, + Inventory: []exporter.InventoryItem{ + { + Name: "Iron Plate", + Amount: 400, + MaxAmount: 800, + }, + }, + }, + { + Name: "Storage Container", + Location: exporter.Location{ + X: 150.0, + Y: 250.0, + Z: -350.0, + }, + Inventory: []exporter.InventoryItem{ + { + Name: "Iron Ore", + Amount: 1200, + MaxAmount: 2400, + }, + }, + }, + }) + }) + + It("records a metric per container with distinct coordinates", func() { + collector.Collect(url, sessionName) + + val1, err := gaugeValue(exporter.StorageInventory, "Iron Plate", "Storage Container", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val1).To(Equal(400.0)) + + val2, err := gaugeValue(exporter.StorageInventory, "Iron Ore", "Storage Container", "150", "250", "-350", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val2).To(Equal(1200.0)) + }) + }) + }) + }) + + Describe("CrateInventoryCollector", func() { + var collector *exporter.CrateInventoryCollector + + BeforeEach(func() { + collector = exporter.NewCrateInventoryCollector("/getCrates") + }) + + AfterEach(func() { + collector = nil + }) + + Describe("Crate inventory metrics", func() { + BeforeEach(func() { + FRMServer.ReturnsCrateData([]exporter.ContainerDetail{ + { + Name: "Death Crate", + Location: exporter.Location{ + X: 75.0, + Y: 125.0, + Z: -175.0, + }, + Inventory: []exporter.InventoryItem{ + { + Name: "Rifle Ammo", + Amount: 50, + MaxAmount: 100, + }, + { + Name: "Health Inhaler", + Amount: 5, + MaxAmount: 10, + }, + }, + }, + }) + }) + + It("records metrics with labels for item name, container name, and coordinates", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.CrateInventory, "Rifle Ammo", "Death Crate", "75", "125", "-175", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current amount as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.CrateInventory, "Rifle Ammo", "Death Crate", "75", "125", "-175", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(50.0)) + }) + + It("records the max amount metric", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.CrateInventoryMax, "Rifle Ammo", "Death Crate", "75", "125", "-175", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(100.0)) + }) + + Describe("when a crate has multiple items", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ammoVal, err := gaugeValue(exporter.CrateInventory, "Rifle Ammo", "Death Crate", "75", "125", "-175", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ammoVal).To(Equal(50.0)) + + inhalerVal, err := gaugeValue(exporter.CrateInventory, "Health Inhaler", "Death Crate", "75", "125", "-175", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(inhalerVal).To(Equal(5.0)) + }) + }) + + Describe("when there are multiple crates", func() { + BeforeEach(func() { + FRMServer.ReturnsCrateData([]exporter.ContainerDetail{ + { + Name: "Death Crate", + Location: exporter.Location{ + X: 75.0, + Y: 125.0, + Z: -175.0, + }, + Inventory: []exporter.InventoryItem{ + { + Name: "Rifle Ammo", + Amount: 50, + MaxAmount: 100, + }, + }, + }, + { + Name: "Dismantle Crate", + Location: exporter.Location{ + X: 200.0, + Y: 300.0, + Z: -400.0, + }, + Inventory: []exporter.InventoryItem{ + { + Name: "Modular Frame", + Amount: 10, + MaxAmount: 20, + }, + }, + }, + }) + }) + + It("records a metric per crate with distinct coordinates", func() { + collector.Collect(url, sessionName) + + val1, err := gaugeValue(exporter.CrateInventory, "Rifle Ammo", "Death Crate", "75", "125", "-175", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val1).To(Equal(50.0)) + + val2, err := gaugeValue(exporter.CrateInventory, "Modular Frame", "Dismantle Crate", "200", "300", "-400", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val2).To(Equal(10.0)) + }) + }) + }) + }) +}) diff --git a/Companion/exporter/inventory_collector_world.go b/Companion/exporter/inventory_collector_world.go new file mode 100644 index 0000000..cb89344 --- /dev/null +++ b/Companion/exporter/inventory_collector_world.go @@ -0,0 +1,31 @@ +package exporter + +import ( + "log" +) + +type WorldInventoryCollector struct { + endpoint string +} + +func NewWorldInventoryCollector(endpoint string) *WorldInventoryCollector { + return &WorldInventoryCollector{ + endpoint: endpoint, + } +} + +func (c *WorldInventoryCollector) Collect(frmAddress string, sessionName string) { + items := []InventoryItem{} + err := retrieveData(frmAddress, c.endpoint, &items) + if err != nil { + log.Printf("error reading inventory statistics from FRM: %s\n", err) + return + } + + for _, item := range items { + WorldInventory.WithLabelValues(item.Name, frmAddress, sessionName).Set(float64(item.Amount)) + WorldInventoryMax.WithLabelValues(item.Name, frmAddress, sessionName).Set(float64(item.MaxAmount)) + } +} + +func (c *WorldInventoryCollector) DropCache() {} diff --git a/Companion/exporter/inventory_detail.go b/Companion/exporter/inventory_detail.go new file mode 100644 index 0000000..c42ef4c --- /dev/null +++ b/Companion/exporter/inventory_detail.go @@ -0,0 +1,14 @@ +package exporter + +type InventoryItem struct { + Name string `json:"Name"` + Amount float64 `json:"Amount"` + MaxAmount float64 `json:"MaxAmount"` +} + +type ContainerDetail struct { + Id string `json:"ID"` + Name string `json:"Name"` + Location Location `json:"location"` + Inventory []InventoryItem `json:"Inventory"` +} diff --git a/Companion/exporter/inventory_metrics.go b/Companion/exporter/inventory_metrics.go new file mode 100644 index 0000000..72c53a0 --- /dev/null +++ b/Companion/exporter/inventory_metrics.go @@ -0,0 +1,83 @@ +package exporter + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +var ( + // Cloud Inventory (Dimensional Depot) + CloudInventory = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "cloud_inventory", + Help: "Items stored in the dimensional depot", + }, []string{ + "item_name", + }) + + CloudInventoryMax = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "cloud_inventory_max", + Help: "Stack size for items in the dimensional depot", + }, []string{ + "item_name", + }) + + // World Inventory + WorldInventory = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "world_inventory", + Help: "Inventory of the world regardless of location (All buildings whom purpose is to provide storage)", + }, []string{ + "item_name", + }) + + WorldInventoryMax = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "world_inventory_max", + Help: "Stack size for items in the world invetory", + }, []string{ + "item_name", + }) + + // Storage Container Inventory + StorageInventory = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "storage_inventory", + Help: "Items stored inside storage containers", + }, []string{ + "item_name", + "container_name", + "x", + "y", + "z", + }) + + StorageInventoryMax = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "storage_inventory_max", + Help: "Stack size for items stored in storage containers", + }, []string{ + "item_name", + "container_name", + "x", + "y", + "z", + }) + + // Crate Inventory (Dismantle and Death Crates) + CrateInventory = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "crate_inventory", + Help: "Items stored inside crates", + }, []string{ + "item_name", + "container_name", + "x", + "y", + "z", + }) + + CrateInventoryMax = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "crate_inventory_max", + Help: "Stack size for items stored in crates", + }, []string{ + "item_name", + "container_name", + "x", + "y", + "z", + }) +) diff --git a/README.md b/README.md index 18b25d1..61ed935 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,31 @@ The [Prometheus metrics server](https://prometheus.io/) allows you to [explore t