Commit 7eca1b43 by huangzhihong

实现 路由忽略,默认忽略+环境变量(SKYWALKING_TRACEIGNOREPATH)扩展忽略

parent 2052f7ef
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
"QueueSize": 30000, "QueueSize": 30000,
"BatchSize": 3000, "BatchSize": 3000,
"gRPC": { "gRPC": {
"Servers": "118.24.155.252:11800", "Servers": "skywalking.service.bailuntec.com:8081",
"Timeout": 10000, "Timeout": 10000,
"ConnectTimeout": 10000, "ConnectTimeout": 10000,
"ReportTimeout": 600000 "ReportTimeout": 600000
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
"QueueSize": 30000, "QueueSize": 30000,
"BatchSize": 3000, "BatchSize": 3000,
"gRPC": { "gRPC": {
"Servers": "118.24.155.252:11800", "Servers": "skywalking.service.bailuntec.com:8081",
"Timeout": 10000, "Timeout": 10000,
"ConnectTimeout": 10000, "ConnectTimeout": 10000,
"ReportTimeout": 600000 "ReportTimeout": 600000
......
...@@ -11,6 +11,9 @@ namespace SimpleConsole.MicroServices ...@@ -11,6 +11,9 @@ namespace SimpleConsole.MicroServices
ITask<IEnumerable<string>> GetAsync(string account); ITask<IEnumerable<string>> GetAsync(string account);
[HttpGet("api/values/{id}")]
ITask<string> GetByIdAsync(int id);
[HttpPost("api/values")] [HttpPost("api/values")]
ITask<string> PostAsync([FormContent] string v1, [FormContent] string v2); ITask<string> PostAsync([FormContent] string v1, [FormContent] string v2);
......
...@@ -28,7 +28,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; ...@@ -28,7 +28,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
.UseContentRoot(Directory.GetCurrentDirectory()) .UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureHostConfiguration(config => .ConfigureHostConfiguration(config =>
{ {
config.AddEnvironmentVariables(prefix: "DOTNET_"); config.AddEnvironmentVariables(/*prefix: "DOTNET_"*/);
if (args != null) if (args != null)
{ {
config.AddCommandLine(args); config.AddCommandLine(args);
......
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Serilog; using Serilog;
using SimpleConsole.MicroServices; using SimpleConsole.MicroServices;
using SkyApm.Tracing; using SkyApm.Tracing;
...@@ -23,26 +23,26 @@ namespace SimpleConsole ...@@ -23,26 +23,26 @@ namespace SimpleConsole
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
await Task.Delay(3000); //延迟确保skywalking agent 已初始化成功 await Task.Delay(3000); //延迟确保skywalking agent 已初始化成功
var index = 1; var index = 1;
while (!stoppingToken.IsCancellationRequested && index < 5) while (!stoppingToken.IsCancellationRequested && index < 6)
{ {
var context = _tracingContext.CreateEntrySegmentContext(nameof(Worker), new TextCarrierHeaderCollection(new Dictionary<string, string>())); var context = _tracingContext.CreateEntrySegmentContext(nameof(Worker), new TextCarrierHeaderCollection(new Dictionary<string, string>()));
Log.Logger.SetTraceId(context.TraceId.ToString()); Log.Logger.SetTraceId(context.TraceId.ToString());
await Task.Delay(1000, stoppingToken); //await Task.Delay(1000, stoppingToken);
//**************************TODO:业务逻辑************************** //**************************TODO:业务逻辑**************************
await _userApi.GetAsync(index.ToString()); await _userApi.GetByIdAsync(index);
Log.Logger.Information($"[console] {index}次调用............... "); Log.Logger.Information($"[console] {index}次调用............... ");
//**************************TODO:业务逻辑************************** //**************************TODO:业务逻辑**************************
index++; index++;
context.Span.AddLog(LogEvent.Message($"Worker running at: {DateTime.Now}")); context.Span.AddLog(LogEvent.Message($"Worker running at: {DateTime.Now}"));
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
"QueueSize": 30000, "QueueSize": 30000,
"BatchSize": 3000, "BatchSize": 3000,
"gRPC": { "gRPC": {
"Servers": "118.24.155.252:11800", "Servers": "skywalking.service.bailuntec.com:8081",
"Timeout": 10000, "Timeout": 10000,
"ConnectTimeout": 10000, "ConnectTimeout": 10000,
"ReportTimeout": 600000 "ReportTimeout": 600000
......
...@@ -8,15 +8,21 @@ ...@@ -8,15 +8,21 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Exceptionless" Version="4.3.2027" /> <PackageReference Include="Exceptionless" Version="4.3.2027" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.1.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.1.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.1.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="2.1.1" />
<PackageReference Include="Serilog" Version="2.9.0" /> <PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Debug" Version="1.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="SkyAPM.Agent.AspNetCore" Version="0.9.0" /> <PackageReference Include="SkyAPM.Agent.AspNetCore" Version="0.9.0" />
<PackageReference Include="SkyAPM.Agent.GeneralHost" Version="0.9.0" /> <PackageReference Include="SkyAPM.Agent.GeneralHost" Version="0.9.0" />
<!--<PackageReference Include="SkyAPM.Utilities.DependencyInjection" Version="0.9.0" />--> <!--<PackageReference Include="SkyAPM.Utilities.DependencyInjection" Version="0.9.0" />-->
......
...@@ -6,11 +6,17 @@ namespace Bailun.ServiceFabric.Trace ...@@ -6,11 +6,17 @@ namespace Bailun.ServiceFabric.Trace
{ {
public class Constants public class Constants
{ {
#region 环境变量
public const string Env_Skywalking_TraceIgnorePath = "SKYWALKING_TRACEIGNOREPATH";
#endregion
/// <summary> /// <summary>
/// 全局追踪Id /// 全局追踪Id
/// </summary> /// </summary>
public const string TraceIdPropertyName = "TraceId"; public const string TraceIdPropertyName = "TraceId";
/// <summary> /// <summary>
/// 业务Id /// 业务Id
/// </summary> /// </summary>
......
using Newtonsoft.Json; using Bailun.ServiceFabric.Trace;
using Newtonsoft.Json;
using SkyApm; using SkyApm;
using SkyApm.Common; using SkyApm.Common;
using SkyApm.Diagnostics; using SkyApm.Diagnostics;
...@@ -28,6 +29,10 @@ namespace Bailun.Diagnostics.HttpClient ...@@ -28,6 +29,10 @@ namespace Bailun.Diagnostics.HttpClient
[DiagnosticName("System.Net.Http.Request")] [DiagnosticName("System.Net.Http.Request")]
public void HttpRequest([Property(Name = "Request")] HttpRequestMessage request) public void HttpRequest([Property(Name = "Request")] HttpRequestMessage request)
{ {
if (TracePathFilter.IsIgnore(request.RequestUri.ToString()))
{
return;
}
_segmentContext = _tracingContext.CreateExitSegmentContext(request.RequestUri.ToString(), _segmentContext = _tracingContext.CreateExitSegmentContext(request.RequestUri.ToString(),
$"{request.RequestUri.Host}:{request.RequestUri.Port}", $"{request.RequestUri.Host}:{request.RequestUri.Port}",
new HttpClientICarrierHeaderCollection(request)); new HttpClientICarrierHeaderCollection(request));
......
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Serilog.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Serilog.Extensions.Hosting;
namespace Serilog
{
/// <summary>
/// Extends <see cref="IWebHostBuilder"/> with Serilog configuration methods.
/// </summary>
public static class SerilogWebHostBuilderExtensions
{
/// <summary>
/// Sets Serilog as the logging provider.
/// </summary>
/// <param name="builder">The web host builder to configure.</param>
/// <param name="logger">The Serilog logger; if not supplied, the static <see cref="Serilog.Log"/> will be used.</param>
/// <param name="dispose">When true, dispose <paramref name="logger"/> when the framework disposes the provider. If the
/// logger is not specified but <paramref name="dispose"/> is true, the <see cref="Log.CloseAndFlush()"/> method will be
/// called on the static <see cref="Log"/> class instead.</param>
/// <param name="providers">A <see cref="LoggerProviderCollection"/> registered in the Serilog pipeline using the
/// <c>WriteTo.Providers()</c> configuration method, enabling other <see cref="ILoggerProvider"/>s to receive events. By
/// default, only Serilog sinks will receive events.</param>
/// <returns>The web host builder.</returns>
public static IWebHostBuilder UseSerilog(
this IWebHostBuilder builder,
ILogger logger = null,
bool dispose = false,
LoggerProviderCollection providers = null)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
builder.ConfigureServices(collection =>
{
if (providers != null)
{
collection.AddSingleton<ILoggerFactory>(services =>
{
var factory = new SerilogLoggerFactory(logger, dispose, providers);
foreach (var provider in services.GetServices<ILoggerProvider>())
factory.AddProvider(provider);
return factory;
});
}
else
{
collection.AddSingleton<ILoggerFactory>(services => new SerilogLoggerFactory(logger, dispose));
}
ConfigureServices(collection, logger);
});
return builder;
}
/// <summary>Sets Serilog as the logging provider.</summary>
/// <remarks>
/// A <see cref="WebHostBuilderContext"/> is supplied so that configuration and hosting information can be used.
/// The logger will be shut down when application services are disposed.
/// </remarks>
/// <param name="builder">The web host builder to configure.</param>
/// <param name="configureLogger">The delegate for configuring the <see cref="LoggerConfiguration" /> that will be used to construct a <see cref="Logger" />.</param>
/// <param name="preserveStaticLogger">Indicates whether to preserve the value of <see cref="Log.Logger"/>.</param>
/// <param name="writeToProviders">By default, Serilog does not write events to <see cref="ILoggerProvider"/>s registered through
/// the Microsoft.Extensions.Logging API. Normally, equivalent Serilog sinks are used in place of providers. Specify
/// <c>true</c> to write events to all providers.</param>
/// <returns>The web host builder.</returns>
public static IWebHostBuilder UseSerilog(
this IWebHostBuilder builder,
Action<WebHostBuilderContext, LoggerConfiguration> configureLogger,
bool preserveStaticLogger = false,
bool writeToProviders = false)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (configureLogger == null) throw new ArgumentNullException(nameof(configureLogger));
builder.ConfigureServices((context, collection) =>
{
var loggerConfiguration = new LoggerConfiguration();
LoggerProviderCollection loggerProviders = null;
if (writeToProviders)
{
loggerProviders = new LoggerProviderCollection();
loggerConfiguration.WriteTo.Providers(loggerProviders);
}
configureLogger(context, loggerConfiguration);
var logger = loggerConfiguration.CreateLogger();
ILogger registeredLogger = null;
if (preserveStaticLogger)
{
registeredLogger = logger;
}
else
{
// Passing a `null` logger to `SerilogLoggerFactory` results in disposal via
// `Log.CloseAndFlush()`, which additionally replaces the static logger with a no-op.
Log.Logger = logger;
}
collection.AddSingleton<ILoggerFactory>(services =>
{
var factory = new SerilogLoggerFactory(registeredLogger, true, loggerProviders);
if (writeToProviders)
{
foreach (var provider in services.GetServices<ILoggerProvider>())
factory.AddProvider(provider);
}
return factory;
});
ConfigureServices(collection, logger);
});
return builder;
}
static void ConfigureServices(IServiceCollection collection, ILogger logger)
{
if (collection == null) throw new ArgumentNullException(nameof(collection));
if (logger != null)
{
// This won't (and shouldn't) take ownership of the logger.
collection.AddSingleton(logger);
}
// Registered to provide two services...
var diagnosticContext = new DiagnosticContext(logger);
// Consumed by e.g. middleware
collection.AddSingleton(diagnosticContext);
// Consumed by user code
collection.AddSingleton<IDiagnosticContext>(diagnosticContext);
}
}
}
using Bailun.ServiceFabric.Trace.Middlewares; using Bailun.ServiceFabric.Trace.Middlewares;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Serilog; using Serilog;
using Serilog.AspNetCore; using Serilog.Events;
using System; using System;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Http.Internal;
namespace Microsoft.AspNetCore.Builder namespace Microsoft.AspNetCore.Builder
{ {
public static class ApplicationBuilderExtensions public static class ApplicationBuilderExtensions
{ {
const string DefaultRequestCompletionMessageTemplate =
"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
static LogEventLevel DefaultGetLevel(HttpContext ctx, double _, Exception ex) =>
ex != null
? LogEventLevel.Error
: ctx.Response.StatusCode > 499
? LogEventLevel.Error
: LogEventLevel.Information;
/// <summary> /// <summary>
/// Adds middleware for streamlined request logging. Instead of writing HTTP request information /// Adds middleware for streamlined request logging. Instead of writing HTTP request information
/// like method, path, timing, status code and exception details /// like method, path, timing, status code and exception details
...@@ -60,8 +65,20 @@ namespace Microsoft.AspNetCore.Builder ...@@ -60,8 +65,20 @@ namespace Microsoft.AspNetCore.Builder
// diagnosticContext.Set(Constants.RequestBodyPropertyName, StringExtension.GetBodyString(httpContext.Request)); // diagnosticContext.Set(Constants.RequestBodyPropertyName, StringExtension.GetBodyString(httpContext.Request));
// }; // };
//}; //};
return app.UseMiddleware<TraceLoggingMiddleware>()
.UseSerilogRequestLogging(configureOptions); var opts = new RequestLoggingOptions
{
GetLevel = DefaultGetLevel,
MessageTemplate = DefaultRequestCompletionMessageTemplate
};
configureOptions?.Invoke(opts);
if (opts.MessageTemplate == null)
throw new ArgumentException($"{nameof(opts.MessageTemplate)} cannot be null.");
if (opts.GetLevel == null)
throw new ArgumentException($"{nameof(opts.GetLevel)} cannot be null.");
return app.UseMiddleware<TraceLoggingMiddleware>(opts);
} }
} }
} }
using Microsoft.AspNetCore.Http;
using Serilog;
using Serilog.Events;
using System;
namespace Bailun.ServiceFabric.Trace.Middlewares
{
/// <summary>
/// Contains options for the <see cref="Serilog.AspNetCore.RequestLoggingMiddleware"/>.
/// </summary>
public class RequestLoggingOptions
{
/// <summary>
/// Gets or sets the message template. The default value is
/// <c>"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"</c>. The
/// template can contain any of the placeholders from the default template, names of properties
/// added by ASP.NET Core, and names of properties added to the <see cref="IDiagnosticContext"/>.
/// </summary>
/// <value>
/// The message template.
/// </value>
public string MessageTemplate { get; set; }
/// <summary>
/// A function returning the <see cref="LogEventLevel"/> based on the <see cref="HttpContext"/>, the number of
/// elapsed milliseconds required for handling the request, and an <see cref="Exception" /> if one was thrown.
/// The default behavior returns <see cref="LogEventLevel.Error"/> when the response status code is greater than 499 or if the
/// <see cref="Exception"/> is not null.
/// </summary>
/// <value>
/// A function returning the <see cref="LogEventLevel"/>.
/// </value>
public Func<HttpContext, double, Exception, LogEventLevel> GetLevel { get; set; }
/// <summary>
/// A callback that can be used to set additional properties on the request completion event.
/// </summary>
public Action<IDiagnosticContext, HttpContext> EnrichDiagnosticContext { get; set; }
internal RequestLoggingOptions() { }
}
}
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using Serilog.Events;
using Serilog.Extensions.Hosting;
using Serilog.Parsing;
using SkyApm.Tracing; using SkyApm.Tracing;
using System; using System;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Bailun.ServiceFabric.Trace.Middlewares namespace Bailun.ServiceFabric.Trace.Middlewares
...@@ -10,31 +17,106 @@ namespace Bailun.ServiceFabric.Trace.Middlewares ...@@ -10,31 +17,106 @@ namespace Bailun.ServiceFabric.Trace.Middlewares
class TraceLoggingMiddleware class TraceLoggingMiddleware
{ {
readonly RequestDelegate _next; readonly RequestDelegate _next;
public TraceLoggingMiddleware(RequestDelegate next) readonly DiagnosticContext _diagnosticContext;
readonly MessageTemplate _messageTemplate;
readonly Action<IDiagnosticContext, HttpContext> _enrichDiagnosticContext;
readonly Func<HttpContext, double, Exception, LogEventLevel> _getLevel;
static readonly LogEventProperty[] NoProperties = new LogEventProperty[0];
public TraceLoggingMiddleware(RequestDelegate next, DiagnosticContext diagnosticContext, RequestLoggingOptions options)
{ {
_next = next; if (options == null) throw new ArgumentNullException(nameof(options));
_next = next ?? throw new ArgumentNullException(nameof(next));
_diagnosticContext = diagnosticContext ?? throw new ArgumentNullException(nameof(diagnosticContext));
_getLevel = options.GetLevel;
_enrichDiagnosticContext = options.EnrichDiagnosticContext;
_messageTemplate = new MessageTemplateParser().Parse(options.MessageTemplate);
} }
public async Task Invoke(HttpContext httpContext) public async Task Invoke(HttpContext httpContext)
{ {
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
string uid; var start = Stopwatch.GetTimestamp();
var collector = _diagnosticContext.BeginCollection();
try
{
string uid;
var entrySegmentContextAccessor = httpContext.RequestServices.GetService<IEntrySegmentContextAccessor>();
if (entrySegmentContextAccessor?.Context != null)
{
uid = entrySegmentContextAccessor.Context.TraceId.ToString();
}
else
{
uid = Guid.NewGuid().ToString("N");
}
var entrySegmentContextAccessor = httpContext.RequestServices.GetService<IEntrySegmentContextAccessor>(); Serilog.Log.Logger.SetTraceId(uid);
if (entrySegmentContextAccessor?.Context != null) await _next(httpContext);
if (TracePathFilter.IsIgnore(GetPath(httpContext)))
{
return;
}
var elapsedMs = GetElapsedMilliseconds(start, Stopwatch.GetTimestamp());
var statusCode = httpContext.Response.StatusCode;
LogCompletion(httpContext, collector, statusCode, elapsedMs, null);
}
catch (Exception ex)
// Never caught, because `LogCompletion()` returns false. This ensures e.g. the developer exception page is still
// shown, although it does also mean we see a duplicate "unhandled exception" event from ASP.NET Core.
when (LogCompletion(httpContext, collector, 500, GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()), ex))
{ {
uid = entrySegmentContextAccessor.Context.TraceId.ToString();
} }
else finally
{ {
uid = Guid.NewGuid().ToString("N"); collector.Dispose();
} }
}
bool LogCompletion(HttpContext httpContext, DiagnosticContextCollector collector, int statusCode, double elapsedMs, Exception ex)
{
var logger = Log.ForContext<TraceLoggingMiddleware>();
var level = _getLevel(httpContext, elapsedMs, ex);
Serilog.Log.Logger.SetTraceId(uid); if (!logger.IsEnabled(level)) return false;
await _next(httpContext); // Enrich diagnostic context
_enrichDiagnosticContext?.Invoke(_diagnosticContext, httpContext);
if (!collector.TryComplete(out var collectedProperties))
collectedProperties = NoProperties;
// Last-in (correctly) wins...
var properties = collectedProperties.Concat(new[]
{
new LogEventProperty("RequestMethod", new ScalarValue(httpContext.Request.Method)),
new LogEventProperty("RequestPath", new ScalarValue(GetPath(httpContext))),
new LogEventProperty("StatusCode", new ScalarValue(statusCode)),
new LogEventProperty("Elapsed", new ScalarValue(elapsedMs))
});
var evt = new LogEvent(DateTimeOffset.Now, level, ex, _messageTemplate, properties);
logger.Write(evt);
return false;
}
static double GetElapsedMilliseconds(long start, long stop)
{
return (stop - start) * 1000 / (double)Stopwatch.Frequency;
}
static string GetPath(HttpContext httpContext)
{
return httpContext.Features.Get<IHttpRequestFeature>()?.RawTarget ?? httpContext.Request.Path.ToString();
} }
} }
} }
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Bailun.ServiceFabric.Trace
{
public class TracePathFilter
{
static string[] DefaultFilterPatterns = new string[]
{
@"log\.bailuntec\.com.*", //过滤exceptionless 上报
@"/status$", //健康心跳检测
@"v1/kv/.*", //Consul DNS
@"swagger/.*" //swagger 文档
};
/// <summary>
/// 是否忽略追踪路由
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static bool IsIgnore(string path)
{
var patterns = Environment
.GetEnvironmentVariable(Constants.Env_Skywalking_TraceIgnorePath)
?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) ?? new string[] { };
patterns = DefaultFilterPatterns.Union(patterns).ToArray();
return patterns.Any(p => Regex.IsMatch(path, p, RegexOptions.IgnoreCase));
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment