当前位置:网站首页>Understanding asp.net core - logging

Understanding asp.net core - logging

2021-11-25 17:56:42 xiaoxiaotank

notes : This article belongs to 《 understand ASP.NET Core》 Series articles , Please check the top blog or Click here to view the full-text catalog

Quick start

Add log provider

In the article host (Host) in , speak of Host.CreateDefaultBuilder Method , The default is to call ConfigureLogging Method added ConsoleDebugEventSource and EventLog( only Windows) There are four logging providers (Logger Provider), And then on the mainframe Build In the process , adopt AddLogging() Registered log related services .

.ConfigureLogging((hostingContext, logging) =>
{
    bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

    if (isWindows)
    {
        logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
    }

    //  add to  Logging  To configure 
    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
    
    // ConsoleLoggerProvider
    logging.AddConsole();
    // DebugLoggerProvider
    logging.AddDebug();
    // EventSourceLoggerProvider
    logging.AddEventSourceLogger();

    if (isWindows)
    {
        //  stay Windows On the platform , add to  EventLogLoggerProvider
        logging.AddEventLog();
    }

    logging.Configure(options =>
    {
        options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
                                            | ActivityTrackingOptions.TraceId
                                            | ActivityTrackingOptions.ParentId;
    });
})

public class HostBuilder : IHostBuilder
{
    private void CreateServiceProvider()
    {
        var services = new ServiceCollection();
        
        // ...
        
        services.AddLogging();
    
        // ...
    }
}

If you don't want to use the log provider added by default , We can go through ClearProviders Clear all added logging providers , Then add what you want , Such as Console

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.ClearProviders()
                .AddConsole();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Log

Logging providers implement interfaces ILoggerProvider, This interface can create ILogger example .

By injecting Services ILogger<TCategoryName>, It is very convenient to log .

The service needs to specify the log category , It could be any string , But we agreed to use the name of the class to which we belong , Through generics . for example , In the controller ValuesController in , The log category is ValuesController The fully qualified type name of the class .

public class ValuesController : ControllerBase
{
    private readonly ILogger<ValuesController> _logger;

    public ValuesController(ILogger<ValuesController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public string Get()
    {
        _logger.LogInformation("ValuesController.Get");
        return "Ok";
    }
}

When requested Get After the method , You can see the output in the console “ValuesController.Get”

If you want to explicitly specify the log category , You can use ILoggerFactory.CreateLogger Method :

public class ValuesController : ControllerBase
{
    private readonly ILogger _logger1;

    public ValuesController(ILoggerFactory loggerFactory)
    {
        _logger1 = loggerFactory.CreateLogger("MyCategory");
    }
}

Configuration log

In the default template , The log configuration is as follows ( stay appsettings.{Environment}.json In file ):

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Configure for all logging providers

LogLevel, seeing the name of a thing one thinks of its function , It refers to the lowest level of logs to be recorded ( That is to record logs greater than or equal to this level ), I think everyone is familiar with . Log levels are described in detail below .

LogLevel In the field , As in the example above “Default”、“Microsoft” etc. , Indicates the category of the log , That is, we inject ILogger Generic parameters specified when . You can set the minimum logging level for each category , That is, the values corresponding to these categories .

The three log categories in the example are explained in detail below .

Default

By default , If the classification is not specially configured ( That is, not in LogLevel Middle configuration ), The application of Default Configuration of .

Microsoft

All categories are in Microsoft All logs at the beginning should be applied Microsoft Configuration of . for example ,Microsoft.AspNetCore.Routing.EndpointMiddleware The configuration will be applied to the logs of the category .

Microsoft.Hosting.Lifetime

All categories are in Microsoft.Hosting.Lifetime All logs at the beginning should be applied Microsoft.Hosting.Lifetime Configuration of . for example , classification Microsoft.Hosting.Lifetime The configuration will be applied , It doesn't apply Microsoft, because Microsoft.Hosting.Lifetime Than Microsoft More specifically .

OK, That's all for the above three log categories .

Back to the example , You may not have noticed , There is no separate configuration for a logging provider ( Such as :Console Record only Error And above , and EventSource You need to record all levels of logs ). Like this , If not configured for a specific logging provider , This configuration will be applied to all logging providers .

Windows EventLog With the exception of .EventLog Must be explicitly configured , Otherwise, it will use its default LogLevel.Warning.

Configure for the specified logging provider

Next, let's look at how to configure for the specified logging provider , Let's start with an example :

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "Console": {
      "LogLevel": {
        "Default": "Error"
      }
    },
    "Debug": {
      "LogLevel": {
        "Microsoft": "None"
      }
    },
    "EventSource": {
      "LogLevel": {
        "Default": "Trace",
        "Microsoft": "Trace",
        "Microsoft.Hosting.Lifetime": "Trace"
      }
    }
  }
}

It's like appsettings.{Environment}.json and appsettings.json The relationship between them is the same ,Logging.{Provider}.LogLevel The configuration in will override Logging.LogLevel Configuration in .

for example Logging.Console.LogLevel.Default Will overwrite Logging.LogLevel.Default,Console The logger will record by default Error And above .

I just mentioned ,Windows EventLog A special , It will not inherit Logging.LogLevel Configuration of .EventLog The default log level is LogLevel.Warning, If you want to modify , Must be specified explicitly , Such as :

{
  "Logging": {
    "EventLog": {
      "LogLevel": {
        "Default": "Information"
      }
    }
  }
}

Configured filtering principle

When creating a ILogger<TCategoryName> Object instance of ,ILoggerFactory According to different logging providers , will :

  1. Find a configuration that matches the logging provider . If you can't find it , General configuration is used .
  2. Then match the configuration category with the longest prefix . If you can't find it , Then use Default To configure .
  3. If multiple configurations are matched , Then use the last .
  4. If no configuration matches , Then use MinimumLevel, This is a configuration item , The default is LogLevel.Information.

Can be in ConfigureLogging In the extension SetMinimumLevel Method setting MinimumLevel.

Log Level

The log level indicates the severity of the log , Divided into 7 etc. , From light to heavy ( final None More special ):

The level of logging value describe
Trace 0 Tracking level , Contains the most detailed information . This information may contain sensitive data , Disabled by default , And must not appear in the production environment .
Debug 1 Debug level , For developers to develop and debug . The amount of information is generally large , Be sure to use with caution in the production environment .
Information 2 Information level , This level is often used .
Warning 3 Warning level , Some unexpected events , But these events do not cause program errors .
Error 4 error level , Some unhandled errors or exceptions , These events cause the current operation or request to fail , But it will not cause errors in the whole application .
Critical 5 Fatal error level , These errors will cause errors in the whole application . For example, there is not enough memory .
None 6 Indicates that no logs are logged

Logging provider

Console

The log will be output to the console .

Debug

The log will be sent through System.Diagnostics.Debug Class to output , Can pass VS Output window view .

stay Linux On , Can be in /var/log/message or /var/log/syslog Find below

EventSource

Cross platform logging , stay Windows I use ETW

Windows EventLog

Only in Windows Effective under the system , It can be done by “ Event viewer ” Log view .

By default

  • LogName by “Application”
  • SourceName by “NET Runtime”
  • MachineName Is the name of the local computer .

These fields can be accessed through EventLogSettings Make changes :

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.AddEventLog(settings =>
            {
                settings.LogName = "My App";
                settings.SourceName = "My Log";
                settings.MachineName = "My Computer";
            })
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Logging filters

Through the logging filter , Allows you to write complex logic , To control whether to log .

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging
                //  For all  LoggerProvider  Set up  Microsoft  Minimum log level , It is recommended to configure through the configuration file 
                .AddFilter("Microsoft", LogLevel.Trace)
                //  in the light of  ConsoleLoggerProvider  Set up  Microsoft  Minimum log level , It is recommended to configure through the configuration file 
                .AddFilter<ConsoleLoggerProvider>("Microsoft", LogLevel.Debug)
                //  For all  LoggerProvider  Filter configuration 
                .AddFilter((provider, category, logLevel) =>
                {
                    //  Because the following is for  ConsoleLoggerProvider  Added filtering configuration , therefore  ConsoleLoggerProvider  Will not enter the method 
                
                    if (provider == typeof(ConsoleLoggerProvider).FullName
                        && category == typeof(ValuesController).FullName
                        && logLevel <= LogLevel.Warning)
                    {
                        // false: Do not log 
                        return false;
                    }

                    // true: Log 
                    return true;
                })
                //  in the light of  ConsoleLoggerProvider  Filter configuration 
                .AddFilter<ConsoleLoggerProvider>((category, logLevel) =>
                {
                    if (category == typeof(ValuesController).FullName
                        && logLevel <= LogLevel.Warning)
                    {
                        // false: Do not log 
                        return false;
                    }

                    // true: Log 
                    return true;
                });
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Log message template

In the process of application development , For a certain type of log , We want their message formats to be consistent , Just some parameters change . This requires the log message template .

for instance :

[HttpGet("{id}")]
public int Get(int id)
{
    _logger.LogInformation("Get {Id}", id);

    return id;
}

among Get {Id} Is a log message template ,{Id} Is the template parameter ( Be careful , Please write your name in it , Not Numbers , This makes it easier to understand the meaning of parameters ).

however , It should be noted that ,{Id} This template parameter , It is only used to make its meaning easy to understand , It has nothing to do with the following parameter name , Template values care about the order of parameters . for example :

[HttpGet("{id}")]
public int Get(int id)
{
    _logger.LogInformation("Get {Id} at {Time}", DateTime.Now, id);

    return id;
}

Suppose you pass in id = 1, Its output is :Get 11/02/2021 11:42:14 at 1

Log message template is a very important function , Among many open source logging middleware , All of them are used .

Logging during host build

ASP.NET Core The framework does not directly support logging during host build . However, you can log through a separate logging provider , for example , Use a third-party logging provider :Serilog

install Nuget package :Install-Package Serilog.AspNetCore

public static void Main(string[] args)
{
    //  from appsettings.json And read the configuration from the command line parameters 
    var config = new ConfigurationBuilder()
        .AddJsonFile("appsettings.json")
        .AddCommandLine(args)
        .Build();

    //  establish Logger
    Log.Logger = new LoggerConfiguration()
        .WriteTo.Console()                          //  Output to console 
        .WriteTo.File(config["Logging:File:Path"])  //  Output to specified file 
        .CreateLogger();
    try
    {
        CreateHostBuilder(args).Build().Run();
    }
    catch(Exception ex)
    {
        Log.Fatal(ex, "Host terminated unexpectedly");

        throw;
    }
    finally
    {
        Log.CloseAndFlush();
    }
}

appsettings.json

{
  "Logging": {
    "File": {
      "Path": "logs/host.log"
    }
  }
}

Console log format configuration

The console logging provider is essential in our development process , From the above, we have learned that we can pass AddConsole() Add . However, it has great limitations , We can't customize the log format .

therefore , stay .NET 5 in , The console logging provider has been extended , Three log output formats are preset :Json、Simple、Systemd.

actually , There were enumerations before ConsoleLoggerFormat Provides Simple and Systemd Format , However, it cannot be customized , It has been abandoned .

these Formatter All inherited from abstract classes ConsoleFormatter, The abstract class constructor receives a “ name ” Parameters , It is required that its implementation class must have a name . You can use static classes ConsoleFormatterNames Get the names of the three built-in formats .

public abstract class ConsoleFormatter
{
    protected ConsoleFormatter(string name)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
    }

    public string Name { get; }

    public abstract void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter);
}

public static class ConsoleFormatterNames
{
    public const string Simple = "simple";

    public const string Json = "json";

    public const string Systemd = "systemd";
}

You can use AddConsole() when , To configure ConsoleLoggerOptions Of FormatterName attribute , To achieve the purpose of custom format , The default value is “simple”. however , For ease of use ,.NET The framework has encapsulated the three built-in formats for us .

these Formatter All option classes inherit from the option class ConsoleFormatterOptions, The option class contains the following three properties :

public class ConsoleFormatterOptions
{
    //  Enable scope , Default  false
    public bool IncludeScopes { get; set; }

    //  Format the timestamp , Appears at the beginning of the log message 
    //  The default is  null, Don't show timestamp 
    public string TimestampFormat { get; set; }

    //  Whether to set the timestamp time zone to  UTC, The default is false, Local time zone 
    public bool UseUtcTimestamp { get; set; }
}

SimpleConsoleFormatter

By extending the method AddSimpleConsole() Support can be added Simple Console logging provider in , Default behavior vs AddConsole() Agreement .

.ConfigureLogging(logging =>
{
    logging.ClearProviders()
        .AddSimpleConsole();
}

Sample output :

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Repos\WebApplication

in addition , You can go through SimpleConsoleFormatterOptions Make some custom configuration :

.ConfigureLogging(logging =>
{
    logging.ClearProviders()
        .AddSimpleConsole(options => 
        {
            //  A log message is displayed on the same line 
            options.SingleLine = true;
            options.IncludeScopes = true;
            options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
            options.UseUtcTimestamp = false;
        });
}

Sample output :

2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:5000
2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down.
2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development
2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Content root path: C:\Repos\WebApplication

SystemdConsoleFormatter

By extending the method AddSystemdConsole() Support can be added Systemd Console logging provider in . If you are familiar with Linux, Then you must be no stranger to it .

.ConfigureLogging(logging =>
{
    logging.ClearProviders()
        .AddSystemdConsole();
}

Sample output :

<6>Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:5000
<6>Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down.
<6>Microsoft.Hosting.Lifetime[0] Hosting environment: Development
<6>Microsoft.Hosting.Lifetime[0] Content root path: C:\Repos\WebApplication

Ahead <6> Indicates the log level info, If you are interested in understanding Systemd, You can visit Mr. Ruan Yifeng's Systemd Introductory tutorial : Command

JsonConsoleFormatter

By extending the method AddJsonConsole() Support can be added Json Console logging provider in .

.ConfigureLogging(logging =>
{
    logging.ClearProviders()
        .AddJsonConsole(options =>
        {
            options.JsonWriterOptions = new JsonWriterOptions
            {
                //  Enable indent , Looks more comfortable 
                Indented = true
            };
        });
}

Sample output :

{
  "EventId": 0,
  "LogLevel": "Information",
  "Category": "Microsoft.Hosting.Lifetime",
  "Message": "Now listening on: http://localhost:5000",
  "State": {
    "Message": "Now listening on: http://localhost:5000",
    "address": "http://localhost:5000",
    "{OriginalFormat}": "Now listening on: {address}"
  }
}
{
  "EventId": 0,
  "LogLevel": "Information",
  "Category": "Microsoft.Hosting.Lifetime",
  "Message": "Application started. Press Ctrl\u002BC to shut down.",
  "State": {
    "Message": "Application started. Press Ctrl\u002BC to shut down.",
    "{OriginalFormat}": "Application started. Press Ctrl\u002BC to shut down."
  }
}
{
  "EventId": 0,
  "LogLevel": "Information",
  "Category": "Microsoft.Hosting.Lifetime",
  "Message": "Hosting environment: Development",
  "State": {
    "Message": "Hosting environment: Development",
    "envName": "Development",
    "{OriginalFormat}": "Hosting environment: {envName}"
  }
}
{
  "EventId": 0,
  "LogLevel": "Information",
  "Category": "Microsoft.Hosting.Lifetime",
  "Message": "Content root path: C:\\Repos\\WebApplication",
  "State": {
    "Message": "Content root path: C:\\Repos\\WebApplication",
    "contentRoot": "C:\\Repos\\WebApplication",
    "{OriginalFormat}": "Content root path: {contentRoot}"
  }
}

If you add a console recorder in multiple formats at the same time , Then only the last addition will take effect .

The above describes how to set the console logging provider through code , However, I think you should prefer to set the logging provider through configuration . Here is a simple configuration example :

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "Console": {
      "FormatterName": "json",
      "FormatterOptions": {
        "SingleLine": true,
        "IncludeScopes": true,
        "TimestampFormat": "yyyy-MM-dd HH:mm:ss ",
        "UseUtcTimestamp": false,
        "JsonWriterOptions": {
          "Indented": true
        }
      }
    }
  }
}

ILogger<TCategoryName> Object instance creation

Here we are. , I wonder if you will be right ILogger<TCategoryName> There are doubts about the creation of object instances : How it was new What comes out? ?

To solve this problem , Let's start with AddLogging() Start with the extension method :

public static class LoggingServiceCollectionExtensions
{
    public static IServiceCollection AddLogging(this IServiceCollection services)
    {
        return AddLogging(services, builder => { });
    }

    public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggingBuilder> configure)
    {
        services.AddOptions();

        //  Registration form example  ILoggerFactory
        services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());
        //  Registration form example  ILogger<>
        services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));

        //  Batch registration single example  IConfigureOptions<LoggerFilterOptions>
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(
            new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));

        configure(new LoggingBuilder(services));
        return services;
    }
}

You may have guessed , This Logger<> Will not be LoggerFactory Create it ? Why else sign up for this thing ?

take it easy , Let's take a look at ILogger<> The implementation class of the service Logger<>

public interface ILogger
{
    void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);

    //  Check whether logs of this log level can be recorded 
    bool IsEnabled(LogLevel logLevel);

    IDisposable BeginScope<TState>(TState state);
}

public interface ILogger<out TCategoryName> : ILogger
{
}
    
public class Logger<T> : ILogger<T>
{
    //  This instance is used for operation inside the interface implementation 
    private readonly ILogger _logger;

    //  Sure enough , Yes  ILoggerFactory  example 
    public Logger(ILoggerFactory factory)
    {
        //  Do you remember? ? As mentioned above, when explicitly specifying the log category , The same is true for creating  ILogger  Example of 
        _logger = factory.CreateLogger(TypeNameHelper.GetTypeDisplayName(typeof(T), includeGenericParameters: false, nestedTypeDelimiter: '.'));
    }
    
    // ...
}

you 're right , You guessed it , Let's take a look at this LoggerFactory Well ( Just list the core code ):

public interface ILoggerFactory : IDisposable
{
    ILogger CreateLogger(string categoryName);

    void AddProvider(ILoggerProvider provider);
}

public class LoggerFactory : ILoggerFactory
{
    //  For singleton  Logger<>
    private readonly Dictionary<string, Logger> _loggers = new Dictionary<string, Logger>(StringComparer.Ordinal);
    //  Deposit  ILoggerProviderRegistrations
    private readonly List<ProviderRegistration> _providerRegistrations = new List<ProviderRegistration>();
    private readonly object _sync = new object();

    public LoggerFactory(IEnumerable<ILoggerProvider> providers, IOptionsMonitor<LoggerFilterOptions> filterOption, IOptions<LoggerFactoryOptions> options = null)
    {
        // ...

        //  register  ILoggerProviders
        foreach (ILoggerProvider provider in providers)
        {
            AddProviderRegistration(provider, dispose: false);
        }

        // ...
    }

    public ILogger CreateLogger(string categoryName)
    {
        lock (_sync)
        {
            //  If it doesn't exist , be  new
            if (!_loggers.TryGetValue(categoryName, out Logger logger))
            {
                logger = new Logger
                {
                    Loggers = CreateLoggers(categoryName),
                };

                (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers);

                //  Uniinstantiation  Logger<>
                _loggers[categoryName] = logger;
            }

            return logger;
        }
    }
    
    private void AddProviderRegistration(ILoggerProvider provider, bool dispose)
    {
        _providerRegistrations.Add(new ProviderRegistration
        {
            Provider = provider,
            ShouldDispose = dispose
        });
        
        // ...
    }
    
    private LoggerInformation[] CreateLoggers(string categoryName)
    {
        var loggers = new LoggerInformation[_providerRegistrations.Count];
        //  Loop through all  ILoggerProvider
        for (int i = 0; i < _providerRegistrations.Count; i++)
        {
            loggers[i] = new LoggerInformation(_providerRegistrations[i].Provider, categoryName);
        }
        return loggers;
    }
}

Be careful

  • If you want to be in Startup.Configure Method , Inject... Directly on the parameters ILogger<Startup> that will do .
  • Does not support the Startup.ConfigureServices Method used in ILogger, Because at this time DI The container has not been configured .
  • There is no asynchronous logging method . The logging action should be executed quickly , Use asynchronous methods at the expense of performance . If the logging action is time-consuming , As recorded MSSQL in , Then please do not write directly MSSQL. You should consider writing logs to fast storage media first , Such as memory queue , Then it is dumped from memory to... Through the background worker thread MSSQL in .
  • Cannot use logging API Change the logging configuration while the application is running . however , Some configuration providers ( Such as file configuration provider ) Reloads the configuration , This can immediately update the logging configuration .

Summary

  • Host.CreateDefaultBuilder In the method , Default added ConsoleDebugEventSource and EventLog( only Windows) There are four logging providers (Logger Provider).
  • By injecting Services ILogger<TCategoryName>, It is convenient to log .
  • The logging provider can be set by code or configuration , Such as LogLevelFormatterName etc. .
  • You can extend the method AddFilter Add logging filter , Allows you to write complex logic , To control whether to log .
  • Support log message templates .
  • For console logging programs ,.NET Frame built in Simple( Default )、SystemdJson Three log output formats .
  • .NET 6 In the preview version, a new one called “ Compile time logging source generation ” The function of , This function is very practical , If you are interested, you can go first Get to know .
  • Last , Give you some common log open source middleware :

版权声明
本文为[xiaoxiaotank]所创,转载请带上原文链接,感谢
https://chowdera.com/2021/11/20211109094145396u.html

随机推荐