From 9b304cc725018e83d4d1ea726e6ac8502f5a2949 Mon Sep 17 00:00:00 2001 From: Johnny Walker Date: Mon, 17 Aug 2026 21:03:46 -0400 Subject: [PATCH 1/3] feat: add per-machine item consumption metrics FRMC already parses each building's Recipe and Ingredients from getFactory, but never exposed consumption as a metric, only production. This adds machine_items_consumed_per_min, machine_items_consumed_max, and machine_items_consumed_pc, mirroring the existing produced-item metrics. Also adds a recipe label to both the produced and consumed machine metrics, sourced from the same already-parsed Recipe field. Without it, there's no way to tell which recipe variant a machine is running when multiple recipes share an output item. --- .../exporter/factory_building_collector.go | 38 ++++++++++++++++++ .../exporter/factory_building_metrics.go | 39 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/Companion/exporter/factory_building_collector.go b/Companion/exporter/factory_building_collector.go index 6cfabba..2a0f348 100644 --- a/Companion/exporter/factory_building_collector.go +++ b/Companion/exporter/factory_building_collector.go @@ -18,6 +18,9 @@ func NewFactoryBuildingCollector(endpoint string) *FactoryBuildingCollector { metricsDropper: NewMetricsDropper( MachineItemsProducedPerMin, MachineItemsProducedEffiency, + MachineItemsConsumedPerMin, + MachineItemsConsumedEffiency, + MachineItemsConsumedMax, ), } } @@ -39,6 +42,7 @@ func (c *FactoryBuildingCollector) Collect(frmAddress string, sessionName string MachineItemsProducedPerMin.WithLabelValues( prod.Name, building.Building, + building.Recipe, strconv.FormatFloat(building.Location.X, 'f', -1, 64), strconv.FormatFloat(building.Location.Y, 'f', -1, 64), strconv.FormatFloat(building.Location.Z, 'f', -1, 64), @@ -48,6 +52,7 @@ func (c *FactoryBuildingCollector) Collect(frmAddress string, sessionName string MachineItemsProducedEffiency.WithLabelValues( prod.Name, building.Building, + building.Recipe, strconv.FormatFloat(building.Location.X, 'f', -1, 64), strconv.FormatFloat(building.Location.Y, 'f', -1, 64), strconv.FormatFloat(building.Location.Z, 'f', -1, 64), @@ -57,6 +62,7 @@ func (c *FactoryBuildingCollector) Collect(frmAddress string, sessionName string MachineItemsProducedMax.WithLabelValues( prod.Name, building.Building, + building.Recipe, strconv.FormatFloat(building.Location.X, 'f', -1, 64), strconv.FormatFloat(building.Location.Y, 'f', -1, 64), strconv.FormatFloat(building.Location.Z, 'f', -1, 64), @@ -64,6 +70,38 @@ func (c *FactoryBuildingCollector) Collect(frmAddress string, sessionName string ).Set(prod.MaxProd) } + for _, ing := range building.Ingredients { + MachineItemsConsumedPerMin.WithLabelValues( + ing.Name, + building.Building, + building.Recipe, + 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(ing.CurrentConsumed) + + MachineItemsConsumedEffiency.WithLabelValues( + ing.Name, + building.Building, + building.Recipe, + 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(ing.ConsPercent) + + MachineItemsConsumedMax.WithLabelValues( + ing.Name, + building.Building, + building.Recipe, + 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(ing.MaxConsumed) + } + for _, item := range building.InputInventory { MachineInputInventory.WithLabelValues( item.Name, diff --git a/Companion/exporter/factory_building_metrics.go b/Companion/exporter/factory_building_metrics.go index 41ac7c8..4e494bf 100644 --- a/Companion/exporter/factory_building_metrics.go +++ b/Companion/exporter/factory_building_metrics.go @@ -11,6 +11,7 @@ var ( }, []string{ "item_name", "machine_name", + "recipe", "x", "y", "z", @@ -22,6 +23,7 @@ var ( }, []string{ "item_name", "machine_name", + "recipe", "x", "y", "z", @@ -33,6 +35,43 @@ var ( }, []string{ "item_name", "machine_name", + "recipe", + "x", + "y", + "z", + }) + + MachineItemsConsumedPerMin = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "machine_items_consumed_per_min", + Help: "How much of an item a building is consuming", + }, []string{ + "item_name", + "machine_name", + "recipe", + "x", + "y", + "z", + }) + + MachineItemsConsumedEffiency = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "machine_items_consumed_pc", + Help: "The efficiency with which a building is consuming an item", + }, []string{ + "item_name", + "machine_name", + "recipe", + "x", + "y", + "z", + }) + + MachineItemsConsumedMax = RegisterNewGaugeVec(prometheus.GaugeOpts{ + Name: "machine_items_consumed_max", + Help: "The maximum of a certain item which the machine can consume", + }, []string{ + "item_name", + "machine_name", + "recipe", "x", "y", "z", From 69a68ea8bcebef0a2e77905e9a369ddb9170c39c Mon Sep 17 00:00:00 2001 From: Johnny Walker Date: Mon, 17 Aug 2026 21:03:52 -0400 Subject: [PATCH 2/3] test: cover new consumption metrics Adds coverage for machine_items_consumed_per_min, _max, and _pc, including a multi-ingredient case. Updates existing assertions for the new recipe label on the produced-item metrics. --- .../factory_building_collector_test.go | 123 ++++++++++++++++-- 1 file changed, 111 insertions(+), 12 deletions(-) diff --git a/Companion/exporter/factory_building_collector_test.go b/Companion/exporter/factory_building_collector_test.go index 41f9a39..d2092bd 100644 --- a/Companion/exporter/factory_building_collector_test.go +++ b/Companion/exporter/factory_building_collector_test.go @@ -50,6 +50,12 @@ var _ = Describe("FactoryBuildingCollector", func() { MaxConsumed: 5.0, ConsPercent: 1.0, }, + { + Name: "Second ingredient", + CurrentConsumed: 2.0, + MaxConsumed: 8.0, + ConsPercent: 0.25, + }, }, ManuSpeed: 100.0, IsConfigured: false, @@ -108,7 +114,7 @@ var _ = Describe("FactoryBuildingCollector", func() { Describe("Machine item 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.MachineItemsProducedPerMin, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + metric, err := getMetric(exporter.MachineItemsProducedPerMin, "Iron Ingot", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(metric).ToNot(BeNil()) }) @@ -116,7 +122,7 @@ var _ = Describe("FactoryBuildingCollector", func() { It("records the current production figure as the metric value", func() { collector.Collect(url, sessionName) - val, err := gaugeValue(exporter.MachineItemsProducedPerMin, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + val, err := gaugeValue(exporter.MachineItemsProducedPerMin, "Iron Ingot", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(val).To(Equal(float64(10.0))) }) @@ -125,11 +131,11 @@ var _ = Describe("FactoryBuildingCollector", func() { It("records a metric per item", func() { collector.Collect(url, sessionName) - ironIngots, err := gaugeValue(exporter.MachineItemsProducedPerMin, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + ironIngots, err := gaugeValue(exporter.MachineItemsProducedPerMin, "Iron Ingot", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(ironIngots).To(Equal(float64(10.0))) - ironNothing, err := gaugeValue(exporter.MachineItemsProducedPerMin, "Iron Nothing", "Smelter", "100", "200", "-300", url, sessionName) + ironNothing, err := gaugeValue(exporter.MachineItemsProducedPerMin, "Iron Nothing", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(ironNothing).To(Equal(float64(1000.0))) }) @@ -139,7 +145,7 @@ 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) + metric, err := getMetric(exporter.MachineItemsProducedMax, "Iron Ingot", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(metric).ToNot(BeNil()) }) @@ -147,7 +153,7 @@ var _ = Describe("FactoryBuildingCollector", func() { 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) + val, err := gaugeValue(exporter.MachineItemsProducedMax, "Iron Ingot", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(val).To(Equal(float64(10))) }) @@ -156,17 +162,110 @@ var _ = Describe("FactoryBuildingCollector", 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) + ironIngots, err := gaugeValue(exporter.MachineItemsProducedMax, "Iron Ingot", "Smelter", "Iron Ingot", "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) + ironNothing, err := gaugeValue(exporter.MachineItemsProducedMax, "Iron Nothing", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(ironNothing).To(Equal(float64(4000.0))) }) }) }) + Describe("Machine item consumption metrics", func() { + It("records a metric with labels for the consumed item name, machine type, recipe, and x, y, z coordinates", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.MachineItemsConsumedPerMin, "Iron Ore", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current consumption figure as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.MachineItemsConsumedPerMin, "Iron Ore", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(float64(5.0))) + }) + + Describe("when a machine has multiple ingredients", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ironOre, err := gaugeValue(exporter.MachineItemsConsumedPerMin, "Iron Ore", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironOre).To(Equal(float64(5.0))) + + secondIngredient, err := gaugeValue(exporter.MachineItemsConsumedPerMin, "Second ingredient", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(secondIngredient).To(Equal(float64(2.0))) + }) + }) + }) + + Describe("Machine item max consumption metrics", func() { + It("records a metric with labels for the consumed item name, machine type, recipe, and x, y, z coordinates", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.MachineItemsConsumedMax, "Iron Ore", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the max consumption as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.MachineItemsConsumedMax, "Iron Ore", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(float64(5.0))) + }) + + Describe("when a machine has multiple ingredients", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ironOre, err := gaugeValue(exporter.MachineItemsConsumedMax, "Iron Ore", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironOre).To(Equal(float64(5.0))) + + secondIngredient, err := gaugeValue(exporter.MachineItemsConsumedMax, "Second ingredient", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(secondIngredient).To(Equal(float64(8.0))) + }) + }) + }) + + Describe("Machine item consumption efficiency metrics", func() { + It("records a metric with labels for the consumed item name, machine type, recipe, and x, y, z coordinates", func() { + collector.Collect(url, sessionName) + metric, err := getMetric(exporter.MachineItemsConsumedEffiency, "Iron Ore", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(metric).ToNot(BeNil()) + }) + + It("records the current consumption efficiency as the metric value", func() { + collector.Collect(url, sessionName) + + val, err := gaugeValue(exporter.MachineItemsConsumedEffiency, "Iron Ore", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(Equal(float64(1.0))) + }) + + Describe("when a machine has multiple ingredients", func() { + It("records a metric per item", func() { + collector.Collect(url, sessionName) + + ironOre, err := gaugeValue(exporter.MachineItemsConsumedEffiency, "Iron Ore", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(ironOre).To(Equal(float64(1.0))) + + secondIngredient, err := gaugeValue(exporter.MachineItemsConsumedEffiency, "Second ingredient", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) + Expect(err).ToNot(HaveOccurred()) + Expect(secondIngredient).To(Equal(float64(0.25))) + }) + }) + }) + 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) @@ -294,7 +393,7 @@ var _ = Describe("FactoryBuildingCollector", func() { 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) - metric, err := getMetric(exporter.MachineItemsProducedEffiency, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + metric, err := getMetric(exporter.MachineItemsProducedEffiency, "Iron Ingot", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(metric).ToNot(BeNil()) }) @@ -302,7 +401,7 @@ var _ = Describe("FactoryBuildingCollector", func() { It("records the current production efficiency as the metric value", func() { collector.Collect(url, sessionName) - val, err := gaugeValue(exporter.MachineItemsProducedEffiency, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + val, err := gaugeValue(exporter.MachineItemsProducedEffiency, "Iron Ingot", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(val).To(Equal(float64(0.5))) }) @@ -311,11 +410,11 @@ var _ = Describe("FactoryBuildingCollector", func() { It("records a metric per item", func() { collector.Collect(url, sessionName) - ironIngots, err := gaugeValue(exporter.MachineItemsProducedEffiency, "Iron Ingot", "Smelter", "100", "200", "-300", url, sessionName) + ironIngots, err := gaugeValue(exporter.MachineItemsProducedEffiency, "Iron Ingot", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(ironIngots).To(Equal(float64(0.5))) - ironNothing, err := gaugeValue(exporter.MachineItemsProducedEffiency, "Iron Nothing", "Smelter", "100", "200", "-300", url, sessionName) + ironNothing, err := gaugeValue(exporter.MachineItemsProducedEffiency, "Iron Nothing", "Smelter", "Iron Ingot", "100", "200", "-300", url, sessionName) Expect(err).ToNot(HaveOccurred()) Expect(ironNothing).To(Equal(float64(0.25))) }) From 363bd4fd353e8ba3ba3e586112d83e423394fd71 Mon Sep 17 00:00:00 2001 From: Johnny Walker Date: Mon, 17 Aug 2026 21:13:22 -0400 Subject: [PATCH 3/3] docs: document new metrics and recipe label Adds the three new machine_items_consumed_* rows and the recipe label on the existing machine_items_produced_* rows. --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 61ed935..feea188 100644 --- a/README.md +++ b/README.md @@ -88,17 +88,32 @@ The [Prometheus metrics server](https://prometheus.io/) allows you to [explore t machine_items_produced_per_min How much of an item a building is producing - item_name, machine_name, x, y, z, url, session_name + item_name, machine_name, recipe, x, y, z, url, session_name machine_items_produced_pc The efficiency with which a building is producing an item - item_name, machine_name, x, y, z, url, session_name + item_name, machine_name, recipe, x, y, z, url, session_name machine_items_produced_max Maximum production rate of an item a building can produce per minute - item_name, machine_name, x, y, z, url, session_name + item_name, machine_name, recipe, x, y, z, url, session_name + + + machine_items_consumed_per_min + How much of an item a building is consuming + item_name, machine_name, recipe, x, y, z, url, session_name + + + machine_items_consumed_pc + The efficiency with which a building is consuming an item + item_name, machine_name, recipe, x, y, z, url, session_name + + + machine_items_consumed_max + Maximum consumption rate of an item a building can consume per minute + item_name, machine_name, recipe, x, y, z, url, session_name machine_input_inventory