首页 > 其他分享 >MASA MAUI APP前端监控指南

MASA MAUI APP前端监控指南

时间:2023-11-30 15:33:41浏览次数:38  
标签:MASA APP Activity Current var SetTag activity MAUI 页面

MAUI Blazor 接入到 OpenTelemetry

近期由于我们APP项目(MAUI+Masa Blazor),需要做运营数据采集埋点,经过综合考虑后,决定采用接入OpenTelemetry SDK的方式,由于目前OpenTelemetry的可测性大部分都是基于后端api的,所以我们也对MAUI Blazor进行接入进行了一番的研究和尝试。

开发工具和环境

  • 开发工具 Visual Studio 2022 Preview (17.8.0 )
  • MAUI 版本:net7.0-ios;net7.0-android
  • .NET Core版本:6.0
  • otel SDK 版本:1.5.1

OpenTelemetry SDK接入过程

  1. MAUI 项目安装OpenTelemetry依赖包:
<PackageReference Include="OpenTelemetry" Version="1.5.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.5.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs" Version="1.5.0-rc.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.5.1" />		
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.5.0-beta.1" />
  1. 注入OpenTelemetry SDK:
    由于当前的OpenTelemetry SDK接入都是针对后端api接口,而MAUI和Blazor前端没有对应的实现,所以需要我们首先需要自定义一个追踪的ActivitySource,然后在相应需要追踪的上下文中使用ActivitySourceActivity进行管理。
//注入全局的 Maui ActivitySource
builder.Services.AddSingleton(new ActivitySource("MAUI"));

//构建OpenTelemetry的tracerProvider
var tracerProvider = Sdk.CreateTracerProviderBuilder()
.ConfigureResource(resource =>
{
    resource.AddService(AppInfo.PackageName, //包名
    AppInfo.Current.Name, //应用名称
    AppInfo.Current.VersionString, //app版本号
    serviceInstanceId: DeviceInfo.Current.Name.ToString()); //设备名称作为instanceId
    resource.AddAttributes(new Dictionary<string, object> {                
        {"device_type", DeviceInfo.Current.DeviceType },//设备类型,物理或虚拟机
        {"device_platform",DeviceInfo.Current.Platform},//设备系统类型,andriod 、ios
        {"device_version",DeviceInfo.Current.Version},// andriod或ios 版本号
        {"device_model",DeviceInfo.Current.Model},//设备型号,不同厂商的手机型号唯一表示
        {"device_manufacturer",DeviceInfo.Current.Manufacturer},//手机厂商
        {"device_idiom",DeviceInfo.Current.Idiom}//终端类型 phone,tv或平板等
    });
})
.AddOtlpExporter(otlp => otlp.Endpoint = new Uri("http://localhost:4317"))
// 把 Maui ActivitySource 添加到OpenTelemetry的追踪源中
.AddSource("MAUI")
.Build();

services.AddSingleton(tracerProvider);
  1. 进行Log的监测:
var resources = ResourceBuilder.CreateDefault().AddService(AppInfo.PackageName, //包名
AppInfo.Current.Name, //应用名称
AppInfo.Current.VersionString, //app版本号
serviceInstanceId: DeviceInfo.Current.Name.ToString()); //设备名称作为instanceId
resources.AddAttributes(new Dictionary<string, object> {
        {"device_type", DeviceInfo.Current.DeviceType },//设备类型,物理或虚拟机
        {"device_platform",DeviceInfo.Current.Platform},//设备系统类型,andriod 、ios
        {"device_version",DeviceInfo.Current.Version},// andriod或ios 版本号
        {"device_model",DeviceInfo.Current.Model},//设备型号,不同厂商的手机型号唯一表示
        {"device_manufacturer",DeviceInfo.Current.Manufacturer},//手机厂商
        {"device_idiom",DeviceInfo.Current.Idiom}//终端类型 phone,tv或平板等
    });
builder.Logging.AddMasaOpenTelemetry(builder =>
{
    builder.SetResourceBuilder(resources);
    builder.AddOtlpExporter(otlp => otlp.Endpoint = new Uri("http://localhost:4317"));
}).SetMinimumLevel(//开发环境记录所有的日志,生产环境只记录错误的日志
#if RELEASE
            LogLevel.Error
#else
            LogLevel.Information
#endif
  1. MAUI的Webview内核UserAgent
    在主MAUI页面MainPage.xmal添加事件BlazorWebViewInitialized:
<BlazorWebView HostPage="wwwroot/index.html" 
  BlazorWebViewInitialized="BlazorWebView_BlazorWebViewInitialized">
    <BlazorWebView.RootComponents>
        <RootComponent Selector="#app" ComponentType="{x:Type blazor:Main}" />
    </BlazorWebView.RootComponents>
</BlazorWebView>

private void BlazorWebView_BlazorWebViewInitialized(object sender, 
  Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializedEventArgs e)
{
    #if ANDROID
    //IPhoneService 为我们构建的ios和android的统一设备设备相关服务
    var phoneService= MauiApplication.Current.Services.GetRequiredService<IPhoneService>();
    phoneService.SetUserAgent(e.WebView.Settings.UserAgentString);
    #endif
}

Blazor页面的对OpenTelemetry的支持

因为Blazor页面和组件有固有的生命周期,所以我们的想法是在生命周期内对Blazor页面和组件进行统一的处理,所以我们构建了当前项目的blazor组件基类 MyCompontentBase,要求所有组件必须继承该类,主要代码:

public abstract partial class MyCompontentBase : IDisposable, IHandleEvent
{
    //基类的logger对象,做日志打印
    [Inject]
    public ILogger Logger { get; set; }

    //前面注入的MAUI ActivitySource实例
    [Inject]
    public ActivitySource activitySource { get; set; }

    #region 事件监听
    //blazor组件事件的委托处理者,用户查找组件类型和相应事件触发的执行方法名称
    private static FieldInfo _delegate = typeof(EventCallbackWorkItem)
                                          .GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
                                          .FirstOrDefault(p => p.Name == "_delegate");

    private CancellationTokenSource _cancellationTokenSource;
    
    // 重写blazor 基类Microsoft.AspNetCore.Components.ComponentBase IHandleEvent的接口
    // 监听所有组件的click事件和监测处理过程中出现的异常信息
    async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
    {
        if (arg is MouseEventArgs mouseEvent && mouseEvent.Type == "click" && _delegate != null)
        {
            var handler = (MulticastDelegate)_delegate.GetValue(callback)!;
            var url = _activity?.GetTagItem("client.path");
            var title = _activity?.GetTagItem("client.title");
            //https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/semantic_conventions/events.md
            Logger.LogInformation("{client.path} {client.title} {event.source.handler} is {event.name}", 
                url, title, handler.Method.Name, mouseEvent.Type);
            //事件触发的组件类型全名称
            _activity?.SetTag("event.source.type", handler.Target.GetType().FullName);
            //事件触发方法的名称,如果为() => {}这类匿名委托方法,这边的记录就没有意义,会生成一个随机的event名称
            _activity?.SetTag("event.source.handler", handler.Method.Name);

            try
            {
                _cancellationTokenSource?.Cancel();
                _cancellationTokenSource = new CancellationTokenSource();

                await Task.Delay(300, _cancellationTokenSource.Token);
                Loading = true;
                await CallBackInvoke();
            }
            catch (TaskCanceledException)
            {

            }
            finally
            {
                Loading = false;
            }
        }
        else
        {
            await CallBackInvoke();
        }

        async Task CallBackInvoke()
        {
            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                                  task.Status != TaskStatus.Canceled;

            if (AfterHandleEventShouldRender())
            {
                StateHasChanged();
            }

            await (shouldAwaitTask
                ? CallStateHasChangedOnAsyncCompletion(task, _activity)
                : Task.CompletedTask);
        }
    }


    //事件执行异常时,将异常信息打印到错误日志
    private async Task CallStateHasChangedOnAsyncCompletion(Task task, Activity activity)
    {
        try
        {
            await task;
        }
        catch (Exception ex) // avoiding exception filters for AOT runtime support
        {
            // Ignore exceptions from task cancellations, but don't bother issuing a state change.
            if (task.IsCanceled)
            {
                return;
            }

            activity?.SetTag("exception.message", ex.Message);
            //如果已经有默认的异常处理,则交给相应的异常处理程序进行处理,否则才打印错误日志
            if (ErrorHandler != null)
            {
                await ErrorHandler.HandleExceptionAsync(ex);
            }
            else
            {
                Logger.LogError(ex, "Compontent execute error , message: {meesage}", ex.Message);
                throw;
            }
        }
        //activity?.Stop();

        if (AfterHandleEventShouldRender())
        {
            StateHasChanged();
        }
    }
    #endregion

    #region 跳转监听
    //监听url地址发生更改时的时间,记录将要跳转的url页面    
    private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        var url = NavigationManager.Uri.Replace(NavigationManager.BaseUri, "/");
        Current?.SetTag("to.path", HttpUtility.UrlDecode(url));
    }
    #endregion

    public long UnixTimespan(DateTime time)
    {
        DateTimeOffset offset = new(time.ToLocalTime());
        return offset.ToUnixTimeMilliseconds();
    }

    /*
     * 当前页面的Activity实例,由于当前只能显示某一个页面,所以为静态对象;
     * 额外把Activity.Current进行同步,利于其它地方的追踪上下文管理。
     */
    public static Activity Current
    {
        get
        {
            return _current;
        }

        set
        {
            _current = value;
            Activity.Current = _current;
        }
    }

    private static Activity _current;

    private static Activity _activity;

    /*
     * 检查当前组件是否页面,
     * 如果页面有RouteAttribute属性,就为页面,否则为组件;
     * 如果是页面会返回页面路由,路由目前是根据路由的参数个数进行匹配,可能不太严谨
     */
    private bool IsPage(out string? routeTemplate)
    {
        routeTemplate = null;
        var routes = GetType().GetCustomAttributes<RouteAttribute>().ToList();
        if (!routes.Any())
            return false;
        if (routes.Count == 1)
            routeTemplate = routes.First().Template;
        else
        {
            var count = NavigationManager.Uri
                            .Replace(NavigationManager.BaseUri, "/").Split('/').Length;
            //根据路由的参数个数进行匹配
            routeTemplate = routes.FirstOrDefault(route => route.Template.Split('/').Length - count == 0)?.Template;
        }
        return true;
    }
    
    //blazor生命周期的第一个执行方法,初始化Activity
    protected override void OnInitialized()
    {
        if (IsPage(out var routeTemplate))
        {
            _activity = StartPageActivity();
            //页面路由,当前我们采用了
            _activity?.SetTag("client.path.route", routeTemplate);
        }
        else
        {
            //HeadToolbar 为我们项目的标题组件,在此获取页面的标题,并写入到Activity
            if (this.GetType() == typeof(HeadToolbar))
            {
                var title = ((HeadToolbar)this).Value;
                _activity?.SetTag("client.title", title);
            }
        }
        //添加url变化监听事件
        NavigationManager.LocationChanged += OnLocationChanged;
        //调用blazor基类的OnInitialized生命周期方法
        base.OnInitialized();
    }

    /*
     * 组件的Activity开始创建的方法,
     * 如果是有上个页面的记录,就将来源页面的标题、url地址和触发的事件的方法名记录下来,
     * 可以追溯从哪个页面的哪个点击,进入到了当前页面
     */
    protected Activity StartPageActivity()
    {
        _activity = activitySource.StartActivity(GetType().Name, ActivityKind.Client);
        if (Current != null && Current != _activity)
        {
            //跳转来源页面的url路径
            _activity?.SetTag("from.path", Current.GetTagItem("client.path"));
            //跳转来源页面的标题
            _activity?.SetTag("from.title", Current.GetTagItem("client.title"));
            //跳转来源页面的点击触发方法名称
            _activity?.SetTag("from.event.source.handler", Current.GetTagItem("event.source.handler"));
            if (string.IsNullOrEmpty(_activity?.ParentId))
                _activity?.SetParentId(Current.Id);
        }
        Current = _activity;
        //客户端类型,做数据筛选可以区分出来是maui blazor的数据
        _activity?.SetTag("client.type", "maui-blazor");
        //userAgent, 如果客户端有特别的问题,可以进行兼容性的排查的信息
        _activity?.SetTag("client.user_agent", PhoneService.UserAgent);

        var url = NavigationManager.Uri.Replace(NavigationManager.BaseUri, "/");
        //当前页面的url地址
        _activity?.SetTag("client.path", HttpUtility.UrlDecode(url));

        return _activity;
    }    
    
    
    protected override async Task OnInitializedAsync()
    {   //我们项目的用户信息会缓存在客户端,在页面加载完成后,异步调用, 可根据项目实际进行适当的调整
        //var user = await LocalStorgeService.GetUserInfoAsync();
        //_activity?.SetTag("enduser.id", user?.Id);
        await base.OnInitializedAsync();
    }
    
    //页面首次加载时,记录页面首次显示时间,可以来观察页面初始化所花费的时间
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            _activity?.SetTag("client.show.startTime", UnixTimespan(DateTime.Now));
        }
        base.OnAfterRender(firstRender);
    }
    
    //页面销毁时,结束追踪,框架会自动上报追踪数据
    protected override void Dispose(bool disposing)
    {
        EndPageActivity();
        NavigationManager.LocationChanged -= OnLocationChanged;
        base.Dispose(disposing);
    }

    private void EndPageActivity()
    {
        _activity?.Stop();
    }
}

我们在Blazor的几个生命周期方法内进行追踪对象Activity的管理:

  1. OnInitialized 进行 当前页面或组件的Activity对象的创建和初始化;
  2. OnInitializedAsync 在页面首次加载完成后,从ILocalStorage获取当前已经登录的用户Id;
  3. OnAfterRender 页面首次渲染完成后,记录页面的首次显示时间;
  4. Dispose 销毁该组件对象时,销毁Activity对象,并自动执行数据上报。

Blazor使用

Blazor页面和组件使用时,必须继承MyCompontentBase,相应的生命周期方法内,必须调用base.对应的生命周期方法;如果有特别的需求,需要向Activity中写入额外的Tag,直接调用Activity.Currrent?.SetTag("tag1","tag1value")即可,如果要打印日志,直接调用Logger.LogInformation("日志内容"),相应的日志和Activity就会被OpenTelemetry SDK自动管理起来

问题

  1. OTEL 默认上报采用Grpc协议,如果部署的OTEL为内网,采用的IP地址加端口,在Andriod 9.0及以上是可以使用的,8.0及以下还需要验证;如果使用了域名和https的方式,则只能在Andriod 10.0及以上版本使用;
  2. 如果使用的是HttpProtobuf协议,则只能在Andriod 10.0及以上版本使用,在Andriod 9.0以内因为HttpClient.Send方法当前存在问题,参考原因,如果想要支持Andriod 9.0及以下版本,可以手动下载OpenTelemetry对象的发布版本源码,修改类BaseOtlpHttpExportClient的方法SendHttpRequest:
protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, 
    CancellationToken cancellationToken)
{
    return this.HttpClient.SendAsync(request, cancellationToken)
                            .GetAwaiter().GetResult();
}

就可以兼容Andriod 9.0以下的数据采集。

实际效果

上述为我们在MAUI + MASA Blazor 移动端项目中引入OpenTelemetry的实践,如果有更好的方式,欢迎与我们讨论沟通。

标签:MASA,APP,Activity,Current,var,SetTag,activity,MAUI,页面
From: https://www.cnblogs.com/MASA/p/17867491.html

相关文章

  • uniapp使用微信jssdk自定义分享
    前言提示:本文记录的是使用uniapp开发的H5+APP项目,H5端使用微信自定义分享功能,文中有关APP的兼容,如果不需要兼容APP的可以忽略一、引入首先安装jweixin-module包npminstalljweixin-module--save二、封装工具方法为了方便使用,新建一个wechat.js文件://#ifdefH5impo......
  • SpringBoot的配置文件application.yml及加载顺序详解
    SpringBoot配置文件application.yml及加载顺序配置文件分类自定义配置文件配置文件总结Springboot中application.yml、application.properties和bootStrap.yml加载顺序SpringApplication位于项目根目录以jar包发布springboot项目时若application.yml和bootStra......
  • Apple开发_Xcode项目中找不到Products文件、无法找到.ipa文件、无法找到打包后的静态
    1、困扰造成的困扰就是找不到.ipa文件了,如果是运行程序用来生成静态库的话,也无法找到.a后或者.framework文件了;编译出的ipa包想直接拿来用,找不到输出的ipa文件。2、解决办法2.1方法一找到项目文件.xcodeproj右击「显示包内容」打开project.pbxproj文件搜索到如下内容:mainGroup=......
  • Flutter App混淆加固、保护与优化原理
    ​ FlutterApp混淆加固、保护与优化原理引言在移动应用程序开发中,保护应用程序的代码和数据安全至关重要。本文将探讨如何对Flutter应用程序进行混淆、优化和保护,以提高应用程序的安全性和隐私。一、混淆原理混淆是一种代码保护技术,通过修改源代码或编译后的代码,使其难以......
  • Flutter App混淆加固、保护与优化原理
    ​ 引言在移动应用程序开发中,保护应用程序的代码和数据安全至关重要。本文将探讨如何对Flutter应用程序进行混淆、优化和保护,以提高应用程序的安全性和隐私。一、混淆原理混淆是一种代码保护技术,通过修改源代码或编译后的代码,使其难以阅读和理解。混淆的主要目的是提高反编......
  • 电视家APP,从此以后电视盒子只是盒子,再与电视毫无关系
    广电总局封掉了电视家APP,于是我决定把我的“当贝盒子”挂咸鱼了,从此以后电视盒子就只是个盒子。  PS:广电的一刀切简直是绝了,绝绝子。              ......
  • 【SpringBoot】单元测试报错java.lang.IllegalStateException: Could not load TestCo
    一、运行test类方法时候报错 二、分析原因,发现版本不一致 三、找到pom文件, 把<version>RELEASE</version>注释掉,刷新一下maven依赖 四:修改后,依赖版本一致。 这样,就可以运行了。 ......
  • 【问题记录】【IDEA】启动突然报错 java: Internal error in the mapping processor:
    1 启动报错换了个高版本的IDEA,启动突然报错:2 解决办法添加编译配置参数:-Djps.track.ap.dependencies=false......
  • APP开发基础源代码分享!
    随着智能手机的普及和移动互联网的发展,APP开发已经成为了一个热门行业,许多企业和个人都希望开发自己的APP,以提供更好的用户体验和服务。然而,对于初学者来说,APP开发可能会面临许多困难和挑战,其中,如何开始APP开发的基础源代码编写是一个非常重要的问题。今天,我们将分享一些APP开发的......
  • [Mac软件]Goldie App v2.2 Mac黄金比例设计工具
    Goldie应用程序是在Mac上测量和可视化黄金比例的最佳方式。从比率的任何一点进行计算,在该行的各个主题之间切换,并自定义您如何使用Goldie。这个紧凑的工具将帮助您轻松创建完美平衡的设计。灵活的尺子Goldie应用程序在您的屏幕上显示一把尺子,这简化了与黄金比例的和解。适应背景颜......