diff --git a/.gitignore b/.gitignore index a0bf66daf..5714d07be 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ /packages/* /Distribution/Configuration/server-access.json + +/grafana-storage/* diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index 13577440d..668ed9cdd 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -417,6 +417,8 @@ public static class Core VerifySerialization(); + Telemetry.Start(); + _now = DateTime.UtcNow; _firstTick = _tickCount = GetTimestamp(); diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index d6b9eb9a2..9d25693fe 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -28,6 +28,7 @@ using Server.Targeting; using Server.Text; using System; using System.Collections.Generic; +using System.Diagnostics.Metrics; using System.Runtime.CompilerServices; using CalcMoves = Server.Movement.Movement; @@ -196,6 +197,10 @@ public delegate int AOSStatusHandler(Mobile from, int index); /// public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyListEntity, IValueLinkListNode { + private static readonly Counter mobilesCreatedCounter = Telemetry.MobilesMeter.CreateCounter("mobiles_created_count"); + private static readonly Counter mobilesDeletedCounter = Telemetry.MobilesMeter.CreateCounter("mobiles_deleted_count"); + private static readonly Counter mobilesKilledCounter = Telemetry.MobilesMeter.CreateCounter("mobiles_killed_count"); + // Allow four warmode changes in 0.5 seconds, any more will be delay for two seconds private const int WarmodeCatchCount = 4; @@ -364,6 +369,11 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro DefaultMobileInit(); World.AddEntity(this); + + mobilesCreatedCounter.Add(1, new KeyValuePair[] + { + new("Map", Map?.Name), + }); } public Mobile(Serial serial) @@ -2421,6 +2431,14 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro Region.OnRegionChange(this, m_Region, null); + if (Alive) { + mobilesDeletedCounter.Add(1, new KeyValuePair[] + { + new("Map", Map?.Name), + new("Region", Region?.Name), + }); + } + m_Region = null; // Is the above line REALLY needed? The old Region system did NOT have said line // and worked fine, because of this a LOT of extra checks have to be done everywhere... @@ -4801,6 +4819,13 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro } } + mobilesKilledCounter.Add(1, new KeyValuePair[] + { + new("Map", Map?.Name), + new("Region", Region?.Name), + new("IsPlayer", Player), + }); + Region.OnDeath(this); OnDeath(c); } diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index a47b3fb1a..0a8690d95 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -35,6 +35,8 @@ + + diff --git a/Projects/Server/Telemetry.cs b/Projects/Server/Telemetry.cs new file mode 100644 index 000000000..7f7cf8a6f --- /dev/null +++ b/Projects/Server/Telemetry.cs @@ -0,0 +1,36 @@ +using System; +using System.Diagnostics.Metrics; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; + +namespace Server; + +public class Telemetry +{ + public static MeterProvider MeterProvider { get; private set; } + public static readonly Meter AccountingMeter = new("ModernUO.Accounting"); + public static readonly Meter ItemsMeter = new("ModernUO.Items"); + public static readonly Meter MobilesMeter = new("ModernUO.Mobiles"); + public static readonly Meter EconomyMeter = new("ModernUO.Economy"); + + public static void Start() + { + MeterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureResource(r => r.AddService("ModernUO")) + .AddOtlpExporter(options => + { + options.BatchExportProcessorOptions.ScheduledDelayMilliseconds = TimeSpan.FromSeconds(10.0).Milliseconds; + }) + .AddMeter("ModernUO.Accounting") + .AddMeter("ModernUO.Items") + .AddMeter("ModernUO.Mobiles") + .AddMeter("ModernUO.Economy") + .AddView( + "gold_created_histogram", + new ExplicitBucketHistogramConfiguration() + { Boundaries = new double[] { 100, 200, 300, 400, 500, 700, 1000, 1500, 2000 } } + ) + .Build(); + } +} diff --git a/Projects/UOContent/Accounting/Account.cs b/Projects/UOContent/Accounting/Account.cs index ac7da1abe..8626a1b63 100644 --- a/Projects/UOContent/Accounting/Account.cs +++ b/Projects/UOContent/Accounting/Account.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Metrics; using System.Net; using System.Runtime.CompilerServices; using System.Xml; @@ -15,6 +16,9 @@ namespace Server.Accounting; [SerializationGenerator(6)] public partial class Account : IAccount, IComparable { + private static readonly Counter accountsCreatedCounter = Telemetry.AccountingMeter.CreateCounter("accounts_created_count"); + private static readonly Counter loginsCounter = Telemetry.AccountingMeter.CreateCounter("logins_count"); + public static readonly TimeSpan YoungDuration = TimeSpan.FromHours(40.0); public static readonly TimeSpan InactiveDuration = TimeSpan.FromDays(180.0); public static readonly TimeSpan EmptyInactiveDuration = TimeSpan.FromDays(30.0); @@ -133,6 +137,8 @@ public partial class Account : IAccount, IComparable Accounts.Add(this); this.MarkDirty(); + + accountsCreatedCounter.Add(1); } public Account(XmlElement node) @@ -794,6 +800,8 @@ public partial class Account : IAccount, IComparable m.SendAsciiMessage($"You will enjoy the benefits and relatively safe status of a young player for {hours} more hours."); } } + + loginsCounter.Add(1, new KeyValuePair("AccessLevel", $"{pm.AccessLevel}")); } public void RemoveYoungStatus(int message) diff --git a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs index 28e423313..ee6e264d5 100644 --- a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs +++ b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Metrics; using System.Reflection; using System.Text.Json; using ModernUO.Serialization; @@ -15,6 +16,9 @@ namespace Server.Engines.Spawners; [SerializationGenerator(10, false)] public abstract partial class BaseSpawner : Item, ISpawner { + private static readonly Counter mobilesSpawnedCounter = Telemetry.MobilesMeter.CreateCounter("mobiles_spawned_count"); + private static readonly Counter itemsSpawnedCounter = Telemetry.ItemsMeter.CreateCounter("items_spawned_count"); + [SerializedIgnoreDupe] [SerializableField(0)] [SerializedCommandProperty(AccessLevel.Developer)] @@ -703,6 +707,12 @@ public abstract partial class BaseSpawner : Item, ISpawner m.Spawner = this; m.OnAfterSpawn(); + + mobilesSpawnedCounter.Add(1, new KeyValuePair[] + { + new("Map", map), + new("Region", m.Region), + }); } else if (entity is Item item) { @@ -717,6 +727,8 @@ public abstract partial class BaseSpawner : Item, ISpawner item.Spawner = this; item.OnAfterSpawn(); + + itemsSpawnedCounter.Add(1, new KeyValuePair("Map", map)); } else { diff --git a/Projects/UOContent/Items/Misc/Gold.cs b/Projects/UOContent/Items/Misc/Gold.cs index af606187f..f5fb77598 100644 --- a/Projects/UOContent/Items/Misc/Gold.cs +++ b/Projects/UOContent/Items/Misc/Gold.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.Metrics; using ModernUO.Serialization; using Server.Accounting; @@ -7,6 +8,10 @@ namespace Server.Items; [SerializationGenerator(0, false)] public partial class Gold : Item { + private static readonly Counter goldCreatedCounter = Telemetry.EconomyMeter.CreateCounter("gold_created_count"); + private static readonly Histogram goldCreatedHistogram = Telemetry.EconomyMeter.CreateHistogram("gold_created_histogram"); + private static readonly Counter goldDeletedCounter = Telemetry.EconomyMeter.CreateCounter("gold_deleted_count"); + [Constructible] public Gold(int amountFrom, int amountTo) : this(Utility.RandomMinMax(amountFrom, amountTo)) { @@ -36,6 +41,19 @@ public partial class Gold : Item var newValue = Amount; UpdateTotal(this, TotalType.Gold, newValue - oldValue); + + if (newValue > 1) + { + goldCreatedCounter.Add(newValue); + goldCreatedHistogram.Record(newValue); + } + } + + public override void Delete() + { + goldDeletedCounter.Add(Amount); + + base.Delete(); } public override void OnAdded(IEntity parent) diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index 5f07339eb..c45d74490 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -44,6 +44,7 @@ + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..96b02f849 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: "2" +services: + # Collector + otel-collector: + image: otel/opentelemetry-collector:0.88.0 + restart: always + command: ["--config=/etc/otel-collector-config.yaml", "${OTELCOL_ARGS}"] + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml + ports: + - "8888:8888" # Prometheus metrics exposed by the collector + - "8889:8889" # Prometheus exporter metrics + - "13133:13133" # health_check extension + - "4317:4317" # OTLP gRPC receiver + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + restart: always + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + + grafana: + image: grafana/grafana + container_name: grafana + restart: unless-stopped + ports: + - '3000:3000' + volumes: + - ./grafana:/etc/grafana/provisioning/datasources + - ./grafana-storage:/var/lib/grafana diff --git a/grafana/datasources.yml b/grafana/datasources.yml new file mode 100644 index 000000000..44999d469 --- /dev/null +++ b/grafana/datasources.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + url: http://prometheus:9090 + isDefault: true + access: proxy + editable: true diff --git a/otel-collector-config.yaml b/otel-collector-config.yaml new file mode 100644 index 000000000..ef0b07a4d --- /dev/null +++ b/otel-collector-config.yaml @@ -0,0 +1,25 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + +exporters: + prometheus: + endpoint: "0.0.0.0:8889" + + debug: + +processors: + batch: + +extensions: + health_check: + +service: + extensions: [health_check] + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [debug, prometheus] diff --git a/prometheus.yaml b/prometheus.yaml new file mode 100644 index 000000000..0120ad33d --- /dev/null +++ b/prometheus.yaml @@ -0,0 +1,6 @@ +scrape_configs: + - job_name: 'otel-collector' + scrape_interval: 10s + static_configs: + - targets: ['otel-collector:8889'] + - targets: ['otel-collector:8888']