#nullable enable using System; using System.Data; using System.Data.Common; using System.Threading; using System.Threading.Tasks; using App.Metrics; using NodaTime; using Npgsql; using Serilog; namespace PluralKit.Core { public class PKCommand: DbCommand, IPKCommand { public NpgsqlCommand Inner { get; } private readonly PKConnection _ourConnection; private readonly ILogger _logger; private readonly IMetrics _metrics; public PKCommand(NpgsqlCommand inner, PKConnection ourConnection, ILogger logger, IMetrics metrics) { Inner = inner; _ourConnection = ourConnection; _logger = logger.ForContext(); _metrics = metrics; } public override Task ExecuteNonQueryAsync(CancellationToken ct) => LogQuery(Inner.ExecuteNonQueryAsync(ct)); public override Task ExecuteScalarAsync(CancellationToken ct) => LogQuery(Inner.ExecuteScalarAsync(ct)); protected override async Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken ct) => await LogQuery(Inner.ExecuteReaderAsync(behavior, ct)); public override Task PrepareAsync(CancellationToken ct = default) => Inner.PrepareAsync(ct); public override void Cancel() => Inner.Cancel(); protected override DbParameter CreateDbParameter() => Inner.CreateParameter(); public override string CommandText { get => Inner.CommandText; set => Inner.CommandText = value; } public override int CommandTimeout { get => Inner.CommandTimeout; set => Inner.CommandTimeout = value; } public override CommandType CommandType { get => Inner.CommandType; set => Inner.CommandType = value; } public override UpdateRowSource UpdatedRowSource { get => Inner.UpdatedRowSource; set => Inner.UpdatedRowSource = value; } protected override DbParameterCollection DbParameterCollection => Inner.Parameters; protected override DbTransaction? DbTransaction { get => Inner.Transaction; set => Inner.Transaction = value switch { NpgsqlTransaction npg => npg, PKTransaction pk => pk.Inner, _ => throw new ArgumentException($"Can't convert input type {value?.GetType()} to NpgsqlTransaction") }; } public override bool DesignTimeVisible { get => Inner.DesignTimeVisible; set => Inner.DesignTimeVisible = value; } protected override DbConnection? DbConnection { get => Inner.Connection; set => Inner.Connection = value switch { NpgsqlConnection npg => npg, PKConnection pk => pk.Inner, _ => throw new ArgumentException($"Can't convert input type {value?.GetType()} to NpgsqlConnection") }; } public override int ExecuteNonQuery() => throw SyncError(nameof(ExecuteNonQuery)); public override object ExecuteScalar() => throw SyncError(nameof(ExecuteScalar)); protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw SyncError(nameof(ExecuteDbDataReader)); public override void Prepare() => throw SyncError(nameof(Prepare)); private async Task LogQuery(Task task) { var start = SystemClock.Instance.GetCurrentInstant(); try { return await task; } finally { var end = SystemClock.Instance.GetCurrentInstant(); var elapsed = end - start; _logger.Verbose("Executed query {Query} in {ElapsedTime} on connection {ConnectionId}", CommandText, elapsed, _ourConnection.ConnectionId); // One "BCL compatible tick" is 100 nanoseconds var micros = elapsed.BclCompatibleTicks / 10; _metrics.Provider.Timer.Instance(CoreMetrics.DatabaseQuery, new MetricTags("query", CommandText)) .Record(micros, TimeUnit.Microseconds, CommandText); } } private static Exception SyncError(string caller) => throw new Exception($"Executed synchronous IDbCommand function {caller}!"); } }