前言 Spring可观察性团队一直致力于为Spring应用程序添加可观察性支持,该特性将在SpringFramework6和SpringBoot3中更加简单、易用!通过可观测性,能更好的了解系统内部运行状态。metrics,logging和分布式tracing之间的相互连通能更好的推断系统的运行状态,以便调试应用程序中的异常、延迟和性能。 即将发布的SpringBoot3。0。0RC1将包含大量的自动配置,用于使用Micrometer改进metrics,并通过Micrometertracing(以前称为SpringCloudSleuth)提供新的分布式tracing支持。最值得注意的变化是,它将包含对log关联的内置支持,W3C上下文传递将是默认传播类型,我们将支持自动传播元数据,以供tracing基础设施(称为远程包裹)使用,帮助标记观察结果。 MicrometerAPI得到了大量的改进,最重要的变化是我们引入了一个新的API:ObservationAPI。 它创建理念是,希望用户使用单一API就能检测代码,并从中获得多种信息(例如metrics,tracing,logging)。 这篇博文详细介绍了你需要了解的API,以及如何使用它来为你的应用程序提供更多可观测信息。一、MicrometerObservation怎么运行? 我们需要通过ObservationRegistry注册ObservationHandler对象。ObservationHandler仅对支持的Observation。Context实现起作用。Contextand可以通过对观察的生命周期事件作出反应来创建计时器、跨度和日志,例如:观测处理程序仅对受支持的观测实现作出反应。上下文,可以通过对观察的生命周期事件作出反应来创建计时器、跨度和日志,例如:start调用Observationstart()方法启动Observation。stop调用Observationstop()方法停止Observation。error调用Observationerror(exception)方法抛出观测。event调用Observationevent(event)观察时发生的事。scopestarted调用ObservationopenScope()方法时,打开了一个scope。不使用时必须关闭。可以在此创建线程局部变量,这些变量在scope关闭时被清除。scopestopped调用Observation。Scopeclose()方法时停止Observationscope。每当调用这些方法中的任何一个时,都会调用ObservationHandler方法(例如onStart(TextendsObservation。Contextctx)、onStop(TextendsObservation。Contextctx)等)。要在处理方法之间传递状态,可以使用Observation。Context。 Observation状态图如下:ObservationObservationContextContextCreatedStartedStopped ObservationScope状态图如下:ObservationContextScopeStartedScopeClosed 为了调试生产问题,observation需要额外的元数据,例如键值对(也称为tags)。然后,可以使用这些tag来查找所需的数据,从而查询metrics或分布式tracing后端。tag可以是高基数或低基数。 这是MicrometerObservationAPI的一个示例。创建ObservationRegistryObservationRegistryregistryObservationRegistry。create();注册ObservationHandlerregistry。observationConfig()。observationHandler(newMyHandler());创建Observation!Observation。createNotStarted(user。name,registry)。contextualName(gettingusername)。lowCardinalityKeyValue(userType,userType1)假设你有3种用户类型。highCardinalityKeyValue(userId,1234)假设这是一个任意数。observe(()log。info(Hello));这是启动观察、打开范围、运行用户代码、关闭范围和停止观察的快捷方式 重要说明:高基数意味着一对可能值的数量是无限多的。HTTPURL就是这样一个键值的好例子(例如useruser1234,useruser2345等)。低基数意味着键值将具有有限数量的可能值。模板化的HTTPURL(例如user{userId})就是这样一个键值的好例子。 要将observation生命周期操作与observation配置(例如名称以及低基数和高基数标记)分离,可以使用ObservationConvention,它提供了一种覆盖默认命名约定的简单方法。二、构建可观测应用 开始的最简单方法是从https:start。spring。io。确保选择的时SpringBoot3。0。0SNAPSHOT(你可以切换到RC1如果他已经发布)和你喜欢的构建工具。我们将构建一个SpringWebMvc服务端应用程序和一个RestTemplate调用服务器的客户端。下面我们从服务端开始。2。1WebMvc服务启动 因为我们想启动HTTP服务,需要选择org。springframework。boot:springbootstarterweb依赖。要使用Observed切面创建observations,我们需要添加org。springframework。boot:springbootstarteraop依赖。要在应用中添加可观测功能,请选择springbootstarteractuator(并将Micrometer添加到classpath)。Metrics对于Micrometermetrics可以使用Prometheus,只需要添加io。micrometer:micrometerregistryprometheus依赖。Tracing对于使用MicrometerTracing跟踪Tracing上下文传播,我们需要选择一个tracer桥接(tracer用于处理跨度生命周期的库)。我们选择ZipkinBrave然后添加io。micrometer:micrometertracingbridgebrave依赖。对于延迟可视化,我们需要以某种格式将完成的spans发送到服务器。在我们的案例中我们生成了一个符合Zipkincompliant标准的span。为此,我们需要添加io。zipkin。reporter2:zipkinreporterbrave依赖。Logs因为我们在classpath添加了MicrometerTracing,所以logs也是自动关联的(也就是说,它们包含一个惟一的trace标识符)。为了这个演示,我们将日志推送到GrafanaLoki。我们可以通过添加com。github。loki4j:lokilogbackappender:latest。release依赖来实现。重要说明如果你不熟悉tracing,我们需要快速定义两个基本术语。你可以在span中包装任何操作。它具有唯一的spanid,并包含计时信息和一些附加的元数据(键值对)。因为可以从span生成子span,所以整个span树形成一个共享相同traceid(即关联标识符)的trace。 现在我们需要添加一些配置。我们设置actuator和metrics用来生成百分位直方图,并重新定义日志模式以包括跟踪和范围标识符。我们将采样概率设置为1。0,以将所有跟踪发送到延迟分析工具。srcmainresourcesapplication。propertiesserver。port7654spring。application。nameserverAlltracesshouldbesenttolatencyanalysistoolmanagement。tracing。sampling。probability1。0management。endpoints。web。exposure。includeprometheusForExemplarstoworkweneedhistogrambucketsmanagement。metrics。distribution。percentileshistogram。http。server。requeststruetraceIDandspanIdarepredefinedMDCkeyswewantthelogstoincludethemlogging。pattern。level5p〔{spring。application。name:},X{traceId:},X{spanId:}〕 因为我们在本地使用Loki和Tempo运行Grafana,所以我们配置lokilogbackappender,将日志发送到Loki的本地实例。!srcmainresourceslogbackspring。?xmlversion1。0encodingUTF8?configurationincluderesourceorgspringframeworkbootlogginglogbackbase。xmlspringPropertyscopecontextnameappNamesourcespring。application。namehttpurlhttp:localhost:3100lokiapiv1pushurlhttpformatlabelpatternapp{appName},host{HOSTNAME},traceIDX{traceId:NONE},levellevelpatternlabelmessagepattern{FILELOGPATTERN}patternmessagesortByTimetruesortByTimeformatappenderrootlevelINFOrootconfiguration2。2WebMvc服务端代码 是时候编写一些服务器端代码了!我们希望实现应用程序的完全可观察性,包括metrics,tracing和附加logging记录。首先,我们编写一个控制器,打印log并调用service。MyController。javaRestControllerclassMyController{privatestaticfinalLoggerlogLoggerFactory。getLogger(MyController。class);privatefinalMyUserServicemyUserSMyController(MyUserServicemyUserService){this。myUserServicemyUserS}GetMapping(user{userId})StringuserName(PathVariable(userId)StringuserId){log。info(Gotarequest);returnmyUserService。userName(userId);}} 我们想对MyUserServiceuserName方法进行一些详细跟踪观察。由于添加了AOP支持,我们可以使用Observed注解。为此,我们可以注册ObservedAspectbean。MyConfiguration。javaConfiguration(proxyBeanMethodsfalse)classMyConfiguration{TohavetheObservedsupportweneedtoregisterthisaspectBeanObservedAspectobservedAspect(ObservationRegistryobservationRegistry){returnnewObservedAspect(observationRegistry);}}MyUserService。javaServiceclassMyUserService{privatestaticfinalLoggerlogLoggerFactory。getLogger(MyUserService。class);privatefinalRandomrandomnewRandom();Exampleofusinganannotationtoobservemethodsuser。namewillbeusedasametricnamegettingusernamewillbeusedasaspannameuserTypeuserType2willbesetasatagforbothmetricspanObserved(nameuser。name,contextualNamegettingusername,lowCardinalityKeyValues{userType,userType2})StringuserName(StringuserId){log。info(Gettingusernameforuserwithid{},userId);try{Thread。sleep(random。nextLong(200L));simulateslatency}catch(InterruptedExceptione){thrownewRuntimeException(e);}}} 有了这个注释就会创建一个timer、一个longtasktimer和一个span。计时器将命名为user。name,长任务计时器将命名为user。name。active,而span将命名为gettingusername。logs我们则可以创建一个专门的处理程序,为每个观察记录一些日志信息。MyHandler。javaComponentclassMyHandlerimplementsObservationHandlerObservation。Context{privatestaticfinalLoggerlogLoggerFactory。getLogger(MyHandler。class);OverridepublicvoidonStart(Observation。Contextcontext){log。info(Beforerunningtheobservationforcontext〔{}〕,userType〔{}〕,context。getName(),getUserTypeFromContext(context));}OverridepublicvoidonStop(Observation。Contextcontext){log。info(Afterrunningtheobservationforcontext〔{}〕,userType〔{}〕,context。getName(),getUserTypeFromContext(context));}OverridepublicbooleansupportsContext(Observation。Contextcontext){}privateStringgetUserTypeFromContext(Observation。Contextcontext){returnStreamSupport。stream(context。getLowCardinalityKeyValues()。spliterator(),false)。filter(keyValueuserType。equals(keyValue。getKey()))。map(KeyValue::getValue)。findFirst()。orElse(UNKNOWN);}} 注册一个bean打开控制器的可观察性。MyConfiguration。javaConfiguration(proxyBeanMethodsfalse)classMyConfiguration{YoumustsetthismanuallyuntilthisisregisteredinBootBeanFilterRegistrationBeanobservationWebFilter(ObservationRegistryobservationRegistry){FilterRegistrationBeanfilterRegistrationBeannewFilterRegistrationBean(newHttpRequestsObservationFilter(observationRegistry));filterRegistrationBean。setDispatcherTypes(DispatcherType。ASYNC,DispatcherType。ERROR,DispatcherType。FORWARD,DispatcherType。INCLUDE,DispatcherType。REQUEST);filterRegistrationBean。setOrder(Ordered。HIGHESTPRECEDENCE);WeprovidealistofURLsthatwewanttocreateobservationsforfilterRegistrationBean。setUrlPatterns(Collections。singletonList(user));returnfilterRegistrationB}}2。3RestTemplate客户端启动 首先我们添加springbootstarterweb和springbootstarteractuator依赖运行web服务器并添加Micrometer支持。是时候添加可观察性相关功能了!Metrics对于Micrometermetrics可以使用Prometheus,只需要添加io。micrometer:micrometerregistryprometheus依赖。Tracing对于使用MicrometerTracing跟踪Tracing上下文传播,我们需要选择一个tracer桥。我选择OpenTelemetry并添加io。micrometer:micrometertracingbridgeotel依赖。对于延迟可视化,我们需要以某种格式将完成的spans发送到服务器。在我们的案例中我们生成了一个符合OpenZipkincompliant标准span,我们需要添加io。opentelemetry:opentelemetryexporterzipkin依赖。Logs添加com。github。loki4j:lokilogbackappender:latest。release依赖并将日志发送到Loki。现在我们需要添加一些配置。我们添加了与服务器端几乎相同的配置。srcmainresourcesapplication。propertiesserver。port6543spring。application。nameclientAlltracesshouldbesenttolatencyanalysistoolmanagement。tracing。sampling。probability1。0management。endpoints。web。exposure。includeprometheustraceIDandspanIdarepredefinedMDCkeyswewantthelogstoincludethemlogging。pattern。level5p〔{spring。application。name:},X{traceId:},X{spanId:}〕 LokiAppender配置看,起来完全相同:!srcmainresourceslogbackspring。?xmlversion1。0encodingUTF8?configurationincluderesourceorgspringframeworkbootlogginglogbackbase。xmlspringPropertyscopecontextnameappNamesourcespring。application。namehttpurlhttp:localhost:3100lokiapiv1pushurlhttpformatlabelpatternapp{appName},host{HOSTNAME},traceIDX{traceId:NONE},levellevelpatternlabelmessagepattern{FILELOGPATTERN}patternmessagesortByTimetruesortByTimeformatappenderrootlevelINFOrootconfiguration2。4RestTemplate应用代码 我们利用RestTemplate发送一个请求,我们希望实现应用程序的全部可观测性,包括metrics和tracing。首先,我们使用RestTemplateBuilder创建一个RestTemplatebean。MyConfiguration。javaConfiguration(proxyBeanMethodsfalse)classMyConfiguration{IMPORTANT!ToinstrumentRestTemplateyoumustinjecttheRestTemplateBuilderBeanRestTemplaterestTemplate(RestTemplateBuilderbuilder){returnbuilder。build();}} 现在,我们可以编写一个CommandLineRunnerbean,它使用ObservationAPI包装,并向服务器端发送请求。下面的代码片段更详细地描述了API的所有功能。MyConfiguration。javaConfiguration(proxyBeanMethodsfalse)classMyConfiguration{BeanCommandLineRunnermyCommandLineRunner(ObservationRegistryregistry,RestTemplaterestTemplate){RandomhighCardinalityValuesnewRandom();SimulatespotentiallylargenumberofvaluesListStringlowCardinalityValuesArrays。asList(userType1,userType2,userType3);Simulateslownumberofvaluesreturnargs{StringhighCardinalityUserIdString。valueOf(highCardinalityValues。nextLong(100000));ExampleofusingtheObservabilityAPImanuallymy。observationisatechnicalnamethatdoesnotdependonthecontext。Itwillbeusedtonamee。g。MetricsObservation。createNotStarted(my。observation,registry)Lowcardinalitymeansthatthenumberofpotentialvalueswontbebig。Lowcardinalityentrieswillendupine。g。Metrics。lowCardinalityKeyValue(userType,randomUserTypePicker(lowCardinalityValues))Highcardinalitymeansthatthenumberofpotentialvaluescanbelarge。Highcardinalityentrieswillendupine。g。Spans。highCardinalityKeyValue(userId,highCardinalityUserId)commandlinerunnerisacontextualnamethatgivesmoredetailswithintheprovidedcontext。Itwillbeusedtonamee。g。Spans。contextualName(commandlinerunner)Thefollowinglambdawillbeexecutedwithanobservationscope(e。g。alltheMDCentrieswillbepopulatedwithtracinginformation)。Alsotheobservationwillbestarted,stoppedandifanerroroccurreditwillberecordedontheobservation。observe((){log。info(Willsendarequesttotheserver);SincewereinanobservationscopethisloglinewillcontaintracingMDCentries。。。StringresponserestTemplate。getForObject(http:localhost:7654user{userId},String。class,highCardinalityUserId);BootsRestTemplateinstrumentationcreatesachildspanherelog。info(Gotresponse〔{}〕,response);。。。sowillthisline});};}}限制 WebMvcObservability的SpringBootAutoConfiguration尚未完成。因此,我们需要手动设置。有关详细信息,请参阅此issue。为了让SpringBootExemplarsAutoConfiguration正常工作,我们需要等待此PR和此PR合并。在此之前,我们需要手动创建配置。2。5运行 我们已经准备好了这个链接下整个可观测性基础设施的Docker设置。按照以下步骤运行基础架构和两个应用程序。运行示例 使用dockercompose启动他们dockercomposeup1。Prometheus默认地址:〔http:localhost:9090〕(http:localhost:9090)2。Grafana默认地址:〔http:localhost:3000〕(http:localhost:3000) 运行服务器端应用程序。。mvnwspringboot:runpl:server 运行客户端应用程序。。mvnwspringboot:runpl:clientYoushouldseelogstatementssimilartothese:20221004T15:04:55。34502:00INFO〔client,bbe3aea006077640b66d40f3e62f04b9,93b7a150b7e293ef〕92556〔main〕com。example。client。ClientApplication:Willsendarequesttotheserver20221004T15:04:55。38502:00INFO〔client,bbe3aea006077640b66d40f3e62f04b9,93b7a150b7e293ef〕92556〔main〕com。example。client。ClientApplication:Gotresponse〔foo〕 打开Grafana仪表盘,然后单击Logs,Traces,Metrics仪表盘。在这里,您可以选择一个traceID(例如bbe3aea006077640b66d40f3e62f04b9),以从两个应用程序中查找与该traceID对应的所有logs和traces信息。应该会看到以下与同一traceID相关的日志和traces相关视图,你将看到在同一时间范围内发生的metrics。这些指标与HTTP请求处理延迟有关。这些来自使用MicrometerAPI的自动收集的SpringBootWebMvc信息。 注意metrics中的图性,这些是Exemplars。这些是特定轨迹,代表在给定时间间隔内进行的测量。如果单击形状,可以跳到traceID视图查看对应该的trace。单击traceID用Tempo查询要么跳转到自己选择的traceID。您将看到以下屏幕。每个条形代表一个span。我们可以看到完成每个操作所需的时间。如果单击给定的范围,我们可以看到与该特定操作相关的tags(键值元数据)和计时信息。 ! 这就是相关日志视图在Loki中的效果图: 如果想查看Observed注解的方法metrics,可以转到Prometheus视图并找到usernameTimer。如果想查看手动创建的Observationmetrics,请转到Prometheus视图并找到myobservationTimer。2。6与AOT支持一起运行 为了更好地理解SpringBoot如何支持Native,请阅读这篇优秀的博客文章。构建 要构建应用程序,需要配置GraalVM到path。如果使用SDKMan调用如下:sdkinstalljava22。2。r17nik 查看GraalVM快速使用。使用Maven构建,你需要启用native环境:。mvnwPnativecleanpackage运行 先运行服务端。。servertargetserver 然后运行客户端。。clienttargetclient 客户端日志:20221010T12:57:17。71202:00INFO〔client,,〕82009〔main〕com。example。client。ClientApplication:StartingClientApplicationusingJava17。0。4onmarcinprecision5560withPID82009(homemarcinrepoobservabilityblogsbootRc1clienttargetclientstartedbymarcininhomemarcinrepoobservabilityblogsbootRc1)20221010T12:57:17。71202:00INFO〔client,,〕82009〔main〕com。example。client。ClientApplication:Noactiveprofileset,fallingbackto1defaultprofile:default20221010T12:57:17。72302:00INFO〔client,,〕82009〔main〕o。s。b。w。embedded。tomcat。TomcatWebServer:Tomcatinitializedwithport(s):6543(http)20221010T12:57:17。72302:00INFO〔client,,〕82009〔main〕o。apache。catalina。core。StandardService:Startingservice〔Tomcat〕20221010T12:57:17。72302:00INFO〔client,,〕82009〔main〕o。apache。catalina。core。StandardEngine:StartingServletengine:〔ApacheTomcat10。0。23〕20221010T12:57:17。72702:00INFO〔client,,〕82009〔main〕o。a。c。c。C。〔Tomcat〕。〔localhost〕。〔〕:InitializingSpringembeddedWebApplicationContext20221010T12:57:17。72702:00INFO〔client,,〕82009〔main〕w。s。c。ServletWebServerApplicationContext:RootWebApplicationContext:initializationcompletedin15ms20221010T12:57:17。73102:00WARN〔client,,〕82009〔main〕i。m。c。i。binder。jvm。JvmGcMetrics:GCnotificationswillnotbeavailablebecauseMemoryPoolMXBeansarenotprovidedbytheJVM20221010T12:57:17。78102:00INFO〔client,,〕82009〔main〕o。s。b。a。e。web。EndpointLinksResolver:Exposing15endpoint(s)beneathbasepathactuator20221010T12:57:17。78302:00INFO〔client,,〕82009〔main〕o。s。b。w。embedded。tomcat。TomcatWebServer:Tomcatstartedonport(s):6543(http)withcontextpath20221010T12:57:17。78302:00INFO〔client,,〕82009〔main〕com。example。client。ClientApplication:StartedClientApplicationin0。077seconds(processrunningfor0。079)20221010T12:57:17。78402:00INFO〔client,27c1113e4276c4173daec3675f536bf4,e0f2db8b983607d8〕82009〔main〕com。example。client。ClientApplication:Willsendarequesttotheserver20221010T12:57:17。82002:00INFO〔client,27c1113e4276c4173daec3675f536bf4,e0f2db8b983607d8〕82009〔main〕com。example。client。ClientApplication:Gotresponse〔foo〕20221010T12:57:18。96602:00INFO〔client,,〕82009〔nio6543exec1〕o。a。c。c。C。〔Tomcat〕。〔localhost〕。〔〕:InitializingSpringDispatcherServletdispatcherServlet20221010T12:57:18。96602:00INFO〔client,,〕82009〔nio6543exec1〕o。s。web。servlet。DispatcherServlet:InitializingServletdispatcherServlet20221010T12:57:18。96602:00INFO〔client,,〕82009〔nio6543exec1〕o。s。web。servlet。DispatcherServlet:Completedinitializationin0ms 服务端日志:20221010T12:57:07。20002:00INFO〔server,,〕81760〔main〕com。example。server。ServerApplication:StartingServerApplicationusingJava17。0。4onmarcinprecision5560withPID81760(homemarcinrepoobservabilityblogsbootRc1servertargetserverstartedbymarcininhomemarcinrepoobservabilityblogsbootRc1)20221010T12:57:07。20102:00INFO〔server,,〕81760〔main〕com。example。server。ServerApplication:Noactiveprofileset,fallingbackto1defaultprofile:default20221010T12:57:07。21302:00INFO〔server,,〕81760〔main〕o。s。b。w。embedded。tomcat。TomcatWebServer:Tomcatinitializedwithport(s):7654(http)20221010T12:57:07。21302:00INFO〔server,,〕81760〔main〕o。apache。catalina。core。StandardService:Startingservice〔Tomcat〕20221010T12:57:07。21302:00INFO〔server,,〕81760〔main〕o。apache。catalina。core。StandardEngine:StartingServletengine:〔ApacheTomcat10。0。23〕20221010T12:57:07。21702:00INFO〔server,,〕81760〔main〕o。a。c。c。C。〔Tomcat〕。〔localhost〕。〔〕:InitializingSpringembeddedWebApplicationContext20221010T12:57:07。21702:00INFO〔server,,〕81760〔main〕w。s。c。ServletWebServerApplicationContext:RootWebApplicationContext:initializationcompletedin16ms20221010T12:57:07。22202:00WARN〔server,,〕81760〔main〕i。m。c。i。binder。jvm。JvmGcMetrics:GCnotificationswillnotbeavailablebecauseMemoryPoolMXBeansarenotprovidedbytheJVM20221010T12:57:07。27802:00INFO〔server,,〕81760〔main〕o。s。b。a。e。web。EndpointLinksResolver:Exposing15endpoint(s)beneathbasepathactuator20221010T12:57:07。28002:00INFO〔server,,〕81760〔main〕o。s。b。w。embedded。tomcat。TomcatWebServer:Tomcatstartedonport(s):7654(http)withcontextpath20221010T12:57:07。28102:00INFO〔server,,〕81760〔main〕com。example。server。ServerApplication:StartedServerApplicationin0。086seconds(processrunningfor0。088)20221010T12:57:07。63902:00INFO〔server,,〕81760〔nio7654exec1〕o。a。c。c。C。〔Tomcat〕。〔localhost〕。〔〕:InitializingSpringDispatcherServletdispatcherServlet20221010T12:57:07。63902:00INFO〔server,,〕81760〔nio7654exec1〕o。s。web。servlet。DispatcherServlet:InitializingServletdispatcherServlet20221010T12:57:07。64002:00INFO〔server,,〕81760〔nio7654exec1〕o。s。web。servlet。DispatcherServlet:Completedinitializationin1ms20221010T12:57:17。78502:00INFO〔server,,〕81760〔nio7654exec8〕com。example。server。MyHandler:Beforerunningtheobservationforcontext〔http。server。requests〕20221010T12:57:17。78502:00INFO〔server,27c1113e4276c4173daec3675f536bf4,9affba5698490e2d〕81760〔nio7654exec8〕com。example。server。MyController:Gotarequest20221010T12:57:17。82002:00INFO〔server,,〕81760〔nio7654exec8〕com。example。server。MyHandler:Afterrunningtheobservationforcontext〔http。server。requests〕 更多信息可以查看Grafana的metrics和traces。阅读Native支持限制章节查看为什么不能将日志推送到Loki。Native支持限制 日志还不会被推给Loki。更多信息请查看:issue25847。客户端还需要手动配置reflectconfig。js。更多信息,请参阅此PR。三、总结 在这篇博文中,我们成功地向您介绍了微米可观察性API背后的主要概念。我们还向您展示了如何使用observeAPI和annotations自定义可观察。您还可以可视化延迟,查看相关日志,并检查来自SpringBoot应用程序的指标。当然还可以通过使用Springnative中的nativeimages来观察应用程序。