拓展 NLog 优雅的输送日志到 Logstash

在上上篇博客通过对aspnetcore启动前配置做了一些更改,以及对nlog进行了自定义字段,可以把请求记录输送到mysql,正式情况可能不会这么部署。因为近期也在学习elk,所以就打算做一个实例,结合nlog把日志输送到logstash,当然现在有开源的.netcore性能监控系统,但是本文的重点是nlog的拓展以及如何拓展。

现有的工作方式

在nlog中,我们在配置文件targets节点下添加一个target就可以定义一个日志输出目标,当我们需要把日志输送到logstash时,需要添加一个target节点,其type为Network,填上address,layout等等,一般有如下配置

<targets>

  <target xsi:type="Network"

        name="logstash_apiinsight"

        keepConnection="false"

        layout="${customer-ip} ${customer-method} ${customer-path} ${customer-bytes} ${customer-duration}"

        address ="tcp://192.168.93.135:8102"

  >

  </target>

</targets>

<rules>

  <logger name="WebApp.*" minlevel="Trace" writeTo="logstash_apiinsight" />

</rules>

我们可以在layout中定义很多个字段,然后在当logstash接受到数据包时通过grok来依次解析每一个字段,如果对正则比较熟悉这种方式确实能够工作,但是当日志记录的字段数越来越多时,其实是很麻烦的。我个人比较喜欢json,日志通过json发送时,数据更加语义化,在logstash的处理也会容易很多,组织成json发送有什么缺点吗?现在能想到的只有它会多发送属性的名称从而浪费一些资源!因为如果按上面配置的layout,日志的传输过程中是不会发送 message,date,level 这些属性的名称的,在logstash做稍作处理就可以解析这些字段。要注意的是,上面的 layout中声明的字段是不存在的,为了方便测试我们可以直接填数据

layout="127.0.0.1 GET /home/index 2000 50"

此时只要你在WebApp命名空间下记录日志,输送到logstash的始终都是这一行内容

如果您是通过rpm来安装的logstash,那么在 /etc/logstash/conf.d 下新建一个 test.conf 输入下面的内容,同时打开服务器的 8012端口

input {

  tcp {

    port => 8102

  }

}

filter {

  grok {

    match => { "message" => "%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:bytes} %{NUMBER:duration}" }

  }

}

output {

  elasticsearch {

    hosts => "localhost:9200"

    index => "sample"

   }

}

启动.netcore程序,多记几次日志,观察kibana就会有如下输出

640?wx_fmt=png

这里虽然是假数据但是想要真的也很简单,在我的上上一片博客中就多次使用了NLog中LayoutRenderer这个父类来自定义字段

[LayoutRenderer("customer-ip")]

public class ProtocolApiInsightRenderer : LayoutRenderer

{

    protected override void Append(StringBuilder builder, LogEventInfo logEvent)

    {

        builder.Append("127.0.0.1");

    }

}

现有方式有一些问题是很难解决的。如果日志本身有空格呢?我们该寻找哪个字符作为分隔符?再比如当部分字段可空部分字段不可空的时候,当要传三个数字到logstash,结果只传了两个,那grok该怎么解析呢,虽然不一定会遇到这种情况,但也反映了语义化的json是更容易处理的。

拓展NLog

思考的过程

其实到这里我们不难发当我们需要向服务器发送tcp数据包时现现有的工作方式是存在不足的,准确来说是向logstash发送数据包是存在不足的,因为这里本身就是一个 NLog.Targets.NetworkTarget,它的原本目标就是向TCP发送日志,在使用nlog的过程中我们知道在向数据库服务器发送日志的时候可以通过配置文件中的parameter节点表明字段,但是NetworkTarget是不支持这样的方式的

<target name = "db_log" xsi:type="Database"

  dbProvider="MySql.Data.MySqlClient.MySqlConnection, MySql.Data"

  connectionString="${var:connectionString}"

  >

  <commandText>

        insert into log(

        application, logged, level, message,

        logger, callsite, exception, ip, user, servername, url

        ) values(

        @application, @logged, @level, @message,

        @logger, @callsite, @exception, @ip, @user, @servername, @url

        );

  </commandText>

  <parameter name = "@application" layout="${apiinsight-application}" />

  <parameter name = "@logged" layout="${date}" />

  <parameter name = "@level" layout="${level}" />

  <parameter name = "@message" layout="${message}" />

  <parameter name = "@logger" layout="${logger}" />

  <parameter name = "@callSite" layout="${callsite:filename=true}" />

  <parameter name = "@exception" layout="${apiinsight-request-exception}" />

  <parameter name = "@IP" layout="${aspnet-Request-IP}"/>

  <parameter name = "@User" layout="${apiinsight-request-user}"/>

  <parameter name = "@serverName" layout="${apiinsight-request-servername}" />

  <parameter name = "@url" layout="${apiinsight-request-url}" />

</target>

通过观察源码还可以发现这里所有的type都是通过继承Target这个抽象类来定义的,所以现在很容易想到一种方式来拓展,那就是写一个 LogstashTarget 继承Target,但是你会发现这要要做的东西非常多,并且可能需要进一步的阅读NLog的源码,NLog代码量相对其他框架来说以及很少了,但是至少还要做这些工作

1、添加类似parameter的节点

2、TCP连接池和消息发送队列

虽然这两项都可以借鉴 NetworkTarget 和 DatabaseTarget 他们的工作方式,但是很明显这样的代码(在你不修改源码的情况下)你会写两遍,软件设计的基本原则就是尽量拓展少修改。

所以我们很快会想到第二种方法,既然要结合NetworkTarget 和 DatabaseTarget他们两的特点,那我是否可以写一个LogstashTarget继承自NetworkTarget,就避免了了 TCP 连接池和消息发送队列的重复代码,柑橘哪不对!因为还要重复DatabaseTarget的一部分代码!这里是我的思考过程,其实最主要的原因还是想少些一些代码吧,所以我考虑到了第三种方法

拓展不行,将就用着

类型LayoutRenderer从出现到现在我一直都是认为它就是用来自定义字段的,但是观察源码就会发现它的子类非常多,并没有去研究每一个子类都做了什么,但是现在他可以最快的帮助我实现想要的功能。这里我定义了一个抽象类

/// <summary>

///     由于 <see cref="NLog.Targets.NetworkTarget"/> 没有提供 parameter 字段

///     为了更好的把数据组织到 logstash,我们可以在这里自定义字段最终以 json 传输到 logstash

/// </summary>

public abstract class LogstashLayoutRenderer : LayoutRenderer

{

    protected HttpContext httpContext => HttpContextProvider.Current;

    protected async override void Append(StringBuilder builder, LogEventInfo logEvent)

    {

        builder.Append(await ProviderJson());

    }

    protected abstract Task<string> ProviderJson();

}

抽象类LogstashLayoutRenderer通过子类实现的方法ProviderJson向builder写入数据,这种方法最简单是因为我的根本需求是想给Logstash发一个json格式的日志,所以这样也比较好理解,至于接下来我想发这个格式的json还是那个格式的json都可以通过实现该类型来达到我的目标,所以现在的方法依然使用NetworkTarget作为输出目标。 此外这里把 HttpContext放进来似乎有点奇怪,可能当时懒得写那么长HttpContextProvider.Current这样去拿吧,可以在这里看它的代码是怎么实现的 https://www.cnblogs.com/cheesebar/p/9078207.html 。不过这样的做法还有一个缺点就是不能在配置文件中定义想要的字段

Logstash的字段

/// <summary>

///     给 <see cref="NetworkTarget"/> 优雅的自定义字段

/// </summary>

public abstract class LayoutFieldBase

{

    public abstract Task<string> ProviderField();

    public abstract string ProviderFieldName { get; }

    public HttpContext httpContext => HttpContextProvider.Current;

}

在给这个类取名字的时候是LogstashFieldBase好呢还是NetworkFieldBase好呢。纠结中就叫LayoutFieldBase吧,其实他是有两个作用的,一方面LogstashLayoutRenderer的ProviderJson方法会搜集所有的字段组织成json,这些字段都是继承自该接口的,另一方面如果我不仅要把这个相同的日志记录到Logstash可能还要记录到db或者文件。LogstashLayoutRenderer的实现者总是包含很多个LayoutFieldBase,这个是写死的,同时因为可能还要记录到db,那我还为每一个LayoutFieldBase的实现者定义了一个LayoutRendererBase。可以看到Append方法调用子类的实现方法来填写响应的字段值,也就是说子类提供一个LayoutFieldBase就可以避免同样的代码写两遍

/// <summary>

///     已知的是这里通过 <see cref="LayoutFieldBase"/> 给 <see cref="NetworkTarget"/> 优雅的自定义字段

///     但是考虑到有些字段可能同事也要输入到 <see cref="DatabaseTarget"/>,但是相同字段的值获取方式是一样的

///     Append 方法通过代理接口 <see cref="ILayoutProxy"/> 提供的 <see cref="LayoutFieldBase"/> 取值

/// </summary>

public abstract class LayoutRendererBase : LayoutRenderer, ILayoutProxy

{

    public abstract Type LayoutType { get; }


    private LayoutFieldBase _layout;


    public LayoutFieldBase Layout

    {

        get

        {

            if (_layout == null)

            {

                if (HttpContextProvider.Current != null)

                {

                    _layout = HttpContextProvider.Current.RequestServices.GetServices<LayoutFieldBase>().First(t => t.GetType() == LayoutType);

                }

            }

            return _layout;

        }

    }


    protected async override void Append(StringBuilder builder, LogEventInfo logEvent)

    {

        builder.Append(await Layout?.ProviderField());

    }

}

当输出目标非Network的时候,依然可以通LayoutRendererBase的实现者的LayoutRenderer特性在配置文件中应用它,这里看一个例子

[LayoutRenderer("apiinsight-application")]

public class ApplicationApiInsightRenderer : LayoutRendererBase {  
  
public override Type LayoutType => typeof(AppLayout); }

预先定义了一些LayoutFieldBase和LayoutRendererBase

真正进行字段值计算的是左边的这些类型,右边的这些类型通过代理使用左边的类型来提供字段,所有的LayoutFieldBase都在开始时注入到容器

/// <summary>

///     应用程序名称

/// </summary>

public class AppLayout : LayoutFieldBase

{

    public override string ProviderFieldName => "app";

    public async override Task<string> ProviderField()

    {

        if (httpContext != null)

        {

            var env = httpContext.RequestServices?.GetService<IHostingEnvironment>();

            return env.ApplicationName;

        }

        return string.Empty;

    }

}

[LayoutRenderer("apiinsight-application")]

public class ApplicationApiInsightRenderer : LayoutRendererBase

{

    public override Type LayoutType => typeof(AppLayout);

}

640?wx_fmt=png

/// <summary>

///     配合 NLog (Target Network) 注入自定义字段

///     自定义字段都继承自 <see cref="LayoutFieldBase"/>

/// </summary>

public static class LogstashLayoutBaseServiceCollectionExtensions

{

    public static void AddLayoutBase(this IServiceCollection services)

    {

        var layouts = AppDomain.CurrentDomain.GetAssemblies().SelectMany(t => t.GetTypes())

            .Where(t => typeof(LayoutFieldBase).IsAssignableFrom(t) && !t.IsAbstract);


        foreach (var item in layouts)

        {

            services.AddSingleton(typeof(LayoutFieldBase), item);

        }

    }

}


 拓展完成

对于拓展这件事,其实已经做完了,因为接下来的事情是业务相关的,在回想一下通过自定义的LogstashLayoutRenderer组织Json到Logstash。在拓展中已经定义了一些可能用到的字段比如说应用程序名称AppLayout,请求方法MethodLayout等等

asp.netcore接口请求统计

新增的start和time字段

回顾之前我写的博客发现只有请求开始时间和请求消耗时间没有在之前的拓展写进来,所在在这里加进来

/// <summary>

///     请求到达的时间

/// </summary>

public class StartLayout : LayoutFieldBase

{

    public override string ProviderFieldName => "start";


    public async override Task<string> ProviderField()

    {

        if (httpContext != null)

        {

            var _apiInsightsKeys = httpContext.RequestServices.GetService<IApiInsightsKeys>();


            if (httpContext != null)

            {

                if (httpContext.Items.TryGetValue(_apiInsightsKeys.StartTimeName, out var start) == true)

                {

                    return ((DateTime)start).ToString("yyyy/MM/dd hh:mm:ss");

                }

            }

        }

        return string.Empty;

    }

}

/// <summary>

///     请求消耗的时间

/// </summary>

public class TimeLayout : LayoutFieldBase

{

    public override string ProviderFieldName => "interval";


    public async override Task<string> ProviderField()

    {

        if (httpContext != null)

        {

            var _apiInsightsKeys = httpContext.RequestServices.GetService<IApiInsightsKeys>();


            if (httpContext != null)

            {

                if (httpContext.Items.TryGetValue(_apiInsightsKeys.StopWatchName, out var stopWatch) == true)

                {

                    return (stopWatch as Stopwatch).ElapsedMilliseconds.ToString();

                }

            }

        }

        return string.Empty;

    }

}

测试的时候可能我也要看统计有没有成功记录,需要对比数据库和elk,所以数据库依然要写,这里定义相应的LayoutRenderer

[LayoutRenderer("apiinsight-start")]

public class StartApiInsightRenderer : LayoutRendererBase

{

    public override Type LayoutType => typeof(StartLayout);

}

[LayoutRenderer("apiinsight-time")]

public class TimeApiInsightRenderer : LayoutRendererBase

{

    public override Type LayoutType => typeof(TimeLayout);

}


 核心ApiInsightLogstashLayoutRenderer

在调试的时候发现所有的LayoutRenderer都是单例的,所以这边的Layouts其实都只会创建一次,所以性能会比想象的好很多,json就是通过newtonsoft这个裤来创建的。

/// <summary>

///     在 NLog 配置文件中,Network 我们只需要注册一个 Layout,名称就是 logstash-apiinsight

/// </summary>

[LayoutRenderer("logstash-apiinsight")]

public class ApiInsightLogstashLayoutRenderer : LogstashLayoutRenderer

{

    static readonly Type[] LayoutTypes = new[] {

        typeof(StartLayout),

        typeof(TimeLayout),

        typeof(ProtocolLayout),

        typeof(HostLayout),

        typeof(PortLayout),

        typeof(PathLayout),

        typeof(QueryLayout),

        typeof(ClientIPLayout),

        typeof(ServerIPLayout),

        typeof(AuthLayout),

        typeof(HttpStatusLayout),

        typeof(AppLayout),

        typeof(MethodLayout),

    };


    static LayoutFieldBase[] Layouts;


    void Init(IServiceProvider serviceProvider)

    {

        var services = serviceProvider.GetServices<LayoutFieldBase>();


        Layouts = services.Where(t => LayoutTypes.Contains(t.GetType())).ToArray();


        if (Layouts.Length != LayoutTypes.Length)

        {

            throw new Exception(nameof(ApiInsightLogstashLayoutRenderer) + " 的 Layouts 和预定义数目的不匹配");

        }

    }


    protected async override Task<string> ProviderJson()

    {

        if (Layouts == null)

        {

            Init(httpContext.RequestServices);

        }

        var dic = new Dictionary<string, string>();


        foreach (var item in Layouts)

        {

            dic.Add(item.ProviderFieldName, await item.ProviderField());

        }


        var json = JObjectHelper.CreateSimpleJson(dic).Replace(Environment.NewLine, string.Empty);


        return json;

    }

}

 logstash配置

input {

  tcp {

    port => 8102

  }

}


filter{

  json {

    source => "message"

  }


  date {

    match => [ "start", "yyyy/MM/dd HH:mm:ss" ]

  }


  mutate{

    convert => {

      "statusCode" => "integer"

      "interval" => "integer"

      "port" => "integer"

    }

  }


}


output {

  elasticsearch {

    hosts => "localhost:9200"

    index => "core-%{+YYYY.MM.dd}"

   }

}

这里就做了一些字段的类型转换,因为默认的所有字段都是string类型,是不方便统计的。

 nlog配置

<target xsi:type="Network"

      name="logstash_apiinsight"

      keepConnection="false"

      layout="${logstash-apiinsight}"

      address ="tcp://192.168.93.135:8103"

        >

</target>

小结

因为近两天公司事情也比较少,事情做完了就乱捣鼓,在使用nlog的Network向Logstash发送数据的时候发现确实不大好用,所以就思考了这样的一个实现方式,基本的是可以了,但是还有一些功能比如说层级json怎么定义,这其实就是单纯的写代码,很有意思的一件事,如果您也有想法或者觉得我的代码有不好的地方或者可以改进的地方,欢迎一起讨论。

 程序地址:https://github.com/cheesebar/ApiInsights


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/321127.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

曼哈顿距离与切比雪夫距离的转化及prufer序列

目录 曼哈顿距离与切比雪夫距离的相互转化prufer序列 1. 曼哈顿距离 与 切比雪夫距离 的相互转化 曼哈顿距离 |x1−x2||y1−y2|max(x1−x2y1−y2,x1−x2−y1y2,−x1x2y1−y2,−x1x2−y1y2)|x1−x2||y1−y2|max(x1−x2y1−y2,x1−x2−y1y2,−x1x2y1−y2,−x1x2−y1y2)|x_1 - x…

初一模拟赛总结(2019.3.9)

成绩&#xff1a; rank算上了其他大佬 rankrankranknamenamenamescorescorescoreT1T1T1T2T2T2T3T3T3T4T4T4222lyflyflyf230230230100100100303030000100100100333hkyhkyhky909090000000000909090444fyfyfy808080000606060000202020444whdwhdwhd808080202020606060000000666lth…

牛客练习赛69E-子串【树状数组】

正题 题目链接:https://ac.nowcoder.com/acm/contest/7329/E 题目大意 给出一个nnn的排列&#xff0c;求有多少个区间[l,r][l,r][l,r]使得最大值是rrr&#xff0c;最小值是lll。 解题思路 首先对于一个位置的值作为左端点和右端点都有一段合法区间&#xff08;到左边第一个比…

[译]如何在.NET Core中使用System.Drawing?

你大概知道System.Drawing&#xff0c;它是一个执行图形相关任务的流行的API&#xff0c;同时它也不属于.NET Core的一部分。最初是把.NET Core作为云端框架设计的&#xff0c;它不包含非云端相关API。另一方面&#xff0c;.NET Core是跨平台框架&#xff0c;它不包含任何操作系…

【dfs】棋盘变幻

棋盘变幻 题目大意&#xff1a; 有一些棋子&#xff08;黑/白&#xff09;&#xff0c;可以将某一列的棋子颜色反转&#xff0c;要黑棋尽可能小 原题&#xff1a; 题目描述 小G在一个n*m的棋盘上随意放上了一些黑色的棋子&#xff0c;然后又在剩下所有没有放棋子的格子里放…

组合数学与数论函数专题

组合数学专题 专题简介 本专题包含了一些组合数学中常见的套路和方法,如拉格朗日插值,动态规划,容斥原理,狄利克雷卷积,线性筛,杜教筛 等等. 目录 2018 四川省赛GRISAIA (数论分块)HDU 6428 Calculate (狄利克雷卷积,线性筛)BZOJ4559 成绩比较 (动态规划,拉格朗日插值)BZOJ…

关于Visual Studio 2019的前期详情

近日&#xff0c;来自微软公司的 John Montgomery 正式宣布&#xff0c;Visual Studio 2019已进入开发阶段。Montgomery 表示&#xff0c;之所以选择在这个时间点公开这个消息&#xff0c;是因为微软准备在 GitHub 上公开可见的项目&#xff08;包括 .NET 和 Roslyn&#xff09…

nssl1522-简单数数题【dp】

正题 题目大意 nnn个数的一个集合&#xff0c;求一个有多少个子集使得这个子集的所有子集的权值和的和是mmm的倍数 解题思路 考虑dp&#xff0c;选中集合中每一个数的贡献次数是2∣S∣−12^{|S|-1}2∣S∣−1&#xff0c;设fi,j,kf_{i,j,k}fi,j,k​表示选到第iii&#xff0c;现…

蛋糕店

蛋糕店 题目大意&#xff1a; 有n个人&#xff0c;让他们排队&#xff0c;每个人都有一个愤怒值&#xff0c;总值为每个人的愤怒值乘上&#xff08;他在队列中的位置-1&#xff09;加在一起 原题&#xff1a; 题目描述 最近小G新开了一家蛋糕店。开业第一天&#xff0c;一…

开源的,跨平台的.NET机器学习框架ML.NET

微软在Build 2018大会上推出的一款面向.NET开发人员的开源&#xff0c;跨平台机器学习框架ML.NET。 ML.NET将允许.NET开发人员开发他们自己的模型&#xff0c;并将自定义ML集成到他们的应用程序中&#xff0c;而无需事先掌握开发或调整机器学习模型的专业知识。在采用通用机器学…

nssl1519-背包签到题【数论】

正题 题目链接:http://10.156.31.134/contestnew.aspx?cid189 题目大意 nnn种物品&#xff0c;每种aia_iai​个。放进kkk个有序盒子求方案数&#xff08;可以不放完&#xff09; 解题思路 不放完其实就是k1k1k1个盒子&#xff0c;插板法求CkaiaiC_{ka_i}^{a_i}Ckai​ai​​&…

【dfs】相似度

相似度 题目大意&#xff1a; 有两个图&#xff0c;问他们的相似度是多少&#xff08;相似度为连接两个点的相样线的条数&#xff09; 原题&#xff1a; 题目描述 小G通过摆放一些城市和道路构成了一个世界地图。趁着小G出去玩的时候&#xff0c;大G把小G的世界地图上的城…

虚树-树上动态规划的利器

虚树 问题引入 在一类树上动态规划问题中,题目给出的询问往往包含树上的很多各节点,并保证总的点数规模小于某个值. 如果我们直接在整颗树上进行dpdp的话,时间复杂度与询问的次数有关,这显然是不可接受的,如果我们可以找到一种动态规划的方法,使其时间复杂度与询问中点的实际…

牛客练习赛69D-火柴排队【dp】

正题 题目链接:https://ac.nowcoder.com/acm/contest/7329/D 题目大意 nnn个数的序列&#xff0c;排序后让随机kkk个数加上ddd&#xff0c;求依旧满足单调上升的期望概率 解题思路 对于一个位置加上ddd后会让到后面一段范围内都得加上ddd。我们预处理一个lil_ili​表示如果ii…

微软推出Visual Studio Kubernetes工具包预览版

微软表示&#xff0c;利用 Visual Studio Kubernetes 这个工具&#xff0c;使用者可以直接在该环境中&#xff0c;构建 Kubernetes 容器应用程序项目&#xff0c;或者让现有的 .NET 网页应用程序也兼容 Kubernetes。除了公有云基础架构环境要支持 Kubernetes&#xff0c;微软现…

伸展树模板

伸展树模板 #include <iostream> #include <cstdio>#define pr(x) std::cout << #x << " : " << x << std::endlclass SplayTree{ public:struct Node{int val;int size;int tag;Node *father;Node *son[2];Node(int val,Node *…

【DP】Sam数

Sam数 题目大意&#xff1a; 问位数为n&#xff0c;且每一位的数字与相邻数字的差值小于等于2的数有多少个 原题&#xff1a; 题目描述 小G最近发现了一种非常有趣的数&#xff0c;他将这种数称之为Sam数。Sam数具有以下特征&#xff1a;相邻两位的数字之差不超过2。小G还…

基于docker 如何部署surging分布式微服务引擎

1、前言转眼间surging 开源已经有1年了&#xff0c;经过1年的打磨&#xff0c;surging已从最初在window 部署的分布式微服务框架&#xff0c;到现在的可以在docker部署利用rancher 进行服务编排的分布式微服务引擎&#xff0c;再把业务进行剥离&#xff0c; 通过配置路径就能驱…

P4213-[模板]杜教筛(Sum)

正题 题目链接:https://www.luogu.com.cn/problem/P4213 题目大意 给出nnn&#xff0c;求∑i1nφ(i)\sum_{i1}^n\varphi(i)i1∑n​φ(i) 和 ∑i1nμ(i)\sum_{i1}^n\mu(i)i1∑n​μ(i) 解题思路 考虑如何将φ\varphiφ卷起来&#xff0c;我们可以乘上一个函数I(I(x)1)I(I(x)1…

线段树专题-等差子序列 BZOJ-2124

线段树专题-等差子序列 感谢 感谢孙耀峰的线段树PPT,使我获益匪浅. 题目来源 BZOJ−2124BZOJ-2124BZOJ−2124 题意 给出长度为nnn的1−n1-n1−n的排列AAA 问是否存在一组1≤p1≤p2≤...≤pl≤n,l≥31 \le p_1 \le p_2 \le ... \le p_l \le n,l \ge 31≤p1​≤p2​≤...≤p…