У меня есть довольно уникальное мультитенантное приложение, где каждый клиент получает свою собственную базу данных. В настоящее время я использую Serilog с MSSqlServerSink для записи всего в одну базу данных. Я только что получил запрос / требование также войти в отдельные базы данных арендаторов.
Я создал новую реализацию ILogEventSink, которая использует ConcurrentDictionary и ищет существующий приемник, а затем создает новый, если он не существует. .
После развертывания мы получили около 503 ошибок, и единственными симптомами, которые наблюдают наши администраторы, является количество подключений к веб-серверу, которые остаются открытыми в состоянии CLOSE_WAIT. Я ищу дополнительную информацию о причинах CLOSE_WAIT, но единственным большим изменением стало развертывание этого нового приемника журналирования.
Поскольку единственная ссылка на новый приемник сохраняется в моем объекте приемника (и этот объект создается во время app_start), я бы думал , что это сработало бы, но возможно ли, что этот новый приемник каким-то образом привязан к потоку, который обрабатывает текущий запрос и поддерживает этот запрос / соединение живым?
using Serilog.Core;
using Serilog.Events;
using Serilog.Sinks.MSSqlServer;
using System;
using System.Collections.Concurrent;
using System.Linq;
using Context = Logging.Constants.Context;
namespace Web.Logging
{
public class CustomerSink : ILogEventSink
{
private readonly ConcurrentDictionary<string, SinkCache> Sinks;
private readonly ICustomerProvider CustomerProvider;
public CustomerSink( ICustomerProvider customerProvider = null )
{
CustomerProvider = customerProvider ?? Customer.GetProvider();
Sinks = new ConcurrentDictionary<string, SinkCache>();
}
private ILogEventSink CreateSink( Customer customer )
{
var columnOptions = SqlServerOptions.DefaultColumnOptions();
var remove = columnOptions.AdditionalColumns.Where( column => column.ColumnName == Context.ApplicationName || column.ColumnName == Context.CustomerId ).ToList();
foreach( var column in remove )
columnOptions.AdditionalColumns.Remove( column );
columnOptions.Store.Remove( StandardColumn.MessageTemplate );
columnOptions.LogEvent.ExcludeAdditionalProperties = true;
columnOptions.LogEvent.ExcludeStandardColumns = true;
return new MSSqlServerSink(
connectionString: customer.ConnectionString,
tableName: "Event",
batchPostingLimit: 50,
period: TimeSpan.FromSeconds( 5 ),
formatProvider: null,
autoCreateSqlTable: true,
columnOptions: columnOptions,
schemaName: "log"
);
}
private SinkCache CreateSink( string customerId )
{
if( CustomerProvider.GetCustomer( customerId, out var customer ) )
return new SinkCache( customer.ConnectionString, CreateSink( customer ) );
return new SinkCache( null, null );
}
public void Emit( LogEvent logEvent )
{
if( logEvent.Properties.TryGetValue( Context.CustomerId, out var value ) && value is ScalarValue scalar && scalar.Value != null )
{
var cache = Sinks.AddOrUpdate( scalar.Value.ToString(), CreateSink,
( customerId, existing ) =>
{
if( existing.Expiration < DateTime.UtcNow )
{
if( CustomerProvider.GetCustomer( customerId, out var customer ) )
{
if( customer.ConnectionString == existing.ConnectionString )
return new SinkCache( existing.ConnectionString, existing.Sink ); //Just refresh the expiration
if( existing.Sink is IDisposable disposable )
disposable.Dispose();
return new SinkCache( customer.ConnectionString, CreateSink( customer ) );
}
else if( existing.Sink is IDisposable disposable )
disposable.Dispose();
return new SinkCache( null, null );
}
//No change
return existing;
} );
cache.Sink?.Emit( logEvent );
}
}
private class SinkCache
{
public string ConnectionString { get; }
public DateTime Expiration { get; }
public ILogEventSink Sink { get; }
public SinkCache( string connectionString, ILogEventSink sink )
{
ConnectionString = connectionString;
Sink = sink;
Expiration = DateTime.UtcNow.AddMinutes( 2 );
}
}
}
}