Wcf通讯基础框架方案(四)——横切日志

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 在第一篇文章中已经列出了几种日志的概览: 所有的日志都有一个最终基类,来看看这个类: [DataContract(Namespace = "WcfExtension")] [KnownType(typeof(WcfExceptionInfo))] [KnownType(t...

在第一篇文章中已经列出了几种日志的概览:

image

所有的日志都有一个最终基类,来看看这个类:

[DataContract(Namespace = "WcfExtension")]
    [KnownType(typeof(WcfExceptionInfo))]
    [KnownType(typeof(ServerExceptionInfo))]
    [KnownType(typeof(ClientExceptionInfo))]
    [KnownType(typeof(WcfInvokeInfo))]
    [KnownType(typeof(ServerInvokeInfo))]
    [KnownType(typeof(ClientInvokeInfo))]
    [KnownType(typeof(WcfMessageInfo))]
    [KnownType(typeof(ServerMessageInfo))]
    [KnownType(typeof(ClientMessageInfo))]
    [KnownType(typeof(StartInfo))]
    [KnownType(typeof(ServerStartInfo))]
    [KnownType(typeof(ClientStartInfo))]
    public abstract class AbstractLogInfo
    {
        [DataMember]
        [PersistenceColumn(IsIndex = true)]
        public string ID { get; set; }

        [DataMember]
        [PersistenceColumn(IsIndex = true)]
        public string RequestIdentity { get; set; }

        [DataMember]
        [PersistenceColumn(IsIndex = true)]
        public DateTime Time { get; set; }

        [DataMember]
        [PersistenceColumn(IsIndex = true)]
        public string MachineName { get; set; }

        [DataMember]
        [PersistenceColumn(IsIndex = true)]
        public string MachineIP { get; set; }

        [DataMember]
        public string ExtraInfo { get; set; }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            this.GetType().GetProperties().ToList().ForEach(p =>
            {
                var o = p.GetValue(this, null);
                sb.AppendLine(p.Name + ": " + o);
                if (o is Dictionary<string, string>)
                {
                    var dic = o as Dictionary<string, string>;
                    foreach (var key in dic)
                    {
                        sb.AppendLine(" " + key.Key + ": " + key.Value);
                    }
                }

            });

            return sb.ToString();
        }
    }

值得关注的几点:

1) 这里的日志我们会保存在Mongodb中,会有一些Attribute告诉日志服务端,字段是否需要做索引,是否需要分库等等。

2) 每一条日志都会有一个GUID作为ID,这个没什么特别的。但要注意一点,如果服务端执行方法出现异常的话,会把异常ID在Message里面返回给客户端。这个异常ID也就是这条异常日志的ID。客户端只能收到有关服务端异常的Message而不会收到更多的堆栈等信息。原因两个,一减少Fault消息大小,二客户端也不应该知道这么多服务端的信息,客户端也不一定能理解服务端的异常,服务端有自己的异常日志。

3) 每一条日志都会有一个RequestIdentity,这是一个请求的上下文关联字段。从客户端发出请求开始,到服务端处理,再到客户端接收到服务端的反馈消息。其中所有的调用日志、异常日志和消息日志都会有相同的RequestIdentity。也就是说在后台通过这个RequestIdentity可以查询到一条一种类型的日志关联的整个请求过程中的其它日志。打个比方,如果在查看客户端执行日志的时候发现方法执行失败,那么直接可以查看到对应的服务端失败的那条执行日志,以及服务端对应的那个异常日志和客户端对应的异常的日志,如果开启消息日志的话,还可以查到对应的客户端和服务端收发的消息。

4) 每条日志都会有机器名和机器IP地址,以及时间。ExtraInfo存的是一些其它信息,在这里我存的是记录这个日志的方法名称。

每一种类型的日志都会有服务端日志和客户端日志两种,分别实现两个接口:

internal interface IServerInfo
{
    string ServiceName { get; set; }
}

internal interface IClientInfo
{
    string ClientTypeName { get; set; }

    string ContractName { get; set; }
}
我认为,服务端日志需要关注服务的类型,而客户端日志关注的是契约的类型,以及调用这个契约所在的类(比如某个页面的类型名),这样可以方便定位问题。因为服务的实现位置相对固定,而调用服务接口的地方就五花八门了。

再来看看其它集中日志类型增加了哪些东西:

    [DataContract(Namespace = "WcfExtension")]
    public abstract class WcfExceptionInfo : AbstractLogInfo
    {
        [DataMember]
        [PersistenceColumn(IsIndex = true)]
        public string Type { get; set; }

        [DataMember]
        public string Message { get; set; }

        [DataMember]
        public string StackTrace { get; set; }
    }

异常日志记录异常类型、异常消息和堆栈。

    [DataContract(Namespace = "WcfExtension")]
    public abstract class WcfMessageInfo : AbstractLogInfo
    {
        [DataMember]
        public MessageDirection MessageDirection { get; set; }

        [DataMember]
        public string Message { get; set; }
    }
}

消息日志记录完整的消息以及消息的方向。

    [DataContract(Namespace = "WcfExtension")]
    [KnownType(typeof(ApplicationContext))]
    public abstract class WcfInvokeInfo : AbstractLogInfo
    {
        [DataMember]
        public long ExecutionTime { get; set; }

        [DataMember]
        public bool IsSuccessuful { get; set; }

        [DataMember]
        public string MethodName { get; set; }

        [DataMember]
        public ApplicationContext ApplicationContext { get; set; }
    }

调用消息记录方法名、执行方法是否成功、执行时间以及一些上下文信息。

最后的启动日志记录了服务端和启动和客户端第一次获取服务接口的信息:

    [DataContract(Namespace = "WcfExtension")]
    [KnownType(typeof(WcfService))]
    public class ServerStartInfo : StartInfo , IServerInfo
    {
        [DataMember]
        public string ServiceName { get; set; }

        [DataMember]
        public WcfService WcfService { get; set; }
    }

    [DataContract(Namespace = "WcfExtension")]
    public class ClientStartInfo : StartInfo , IClientInfo
    {
        [DataMember]
        public string ContractName { get; set; }

        [DataMember]
        public string ClientTypeName { get; set; }

        [DataMember]
        public WcfEndpoint WcfEndpoint { get; set; }
    }

在上一篇文章中我们看到了,客户端的执行日志、异常日志是在ServiceRealProxy中实现的,而启动日志是在WcfServiceClientFactory中的CreateServiceClient方法实现的。那么消息日志在哪里实现的呢?

internal class ClientMessageInspector : IClientMessageInspector
    {
        private static Dictionary<string, string> contractVersionCache = new Dictionary<string, string>();
        private static object locker = new object();

        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
#if DEBUG
            var message = reply.ToString();
            Console.WriteLine("客户端收到消息:" + message);
#endif
            ClientApplicationContext.Current = reply.GetApplicationContext<ClientApplicationContext>();    

            try
            {
                if (WcfLogManager.Current((correlationState as Type)).MessageInfo.Client.Enabled)
                {
                    var direct = WcfLogManager.Current((correlationState as Type)).MessageInfo.Client.Direction;
                    if (direct == WcfDirection.Both ||
                        direct == WcfDirection.Receive)
                    {
                        var log = WcfLogProvider.GetClientMessageInfo(
                          (correlationState as Type).FullName,
                          ClientApplicationContext.Current.RequestIdentity,
                          "ClientMessageInspector.AfterReceiveReply",
                          MessageDirection.Receive,
                          reply.ToString());

                        WcfServiceLocator.GetLogService().LogWithoutException(log);
                    }
                }
            }
            catch (Exception ex)
            {
                LocalLogService.Log(ex.ToString());
            }
        }

        public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            try
            {
                var channelType = channel.GetType();

                var serverContext = new ServerApplicationContext();
                serverContext.RequestIdentity = Guid.NewGuid().ToString();
                serverContext.ClientMachineName = WcfLogProvider.MachineName;

                if (!contractVersionCache.ContainsKey(channelType.FullName))
                {
                    lock (locker)
                    {
                        if (!contractVersionCache.ContainsKey(channelType.FullName))
                        {
                            contractVersionCache.Add(channelType.FullName, channelType.Assembly.GetName().Version.ToString());
                        }
                    }
                }
                serverContext.ClientVersion = contractVersionCache[channelType.FullName];
                request.SetApplicationContext(serverContext);

                var clientContext = new ClientApplicationContext();
                clientContext.RequestIdentity = serverContext.RequestIdentity;
                ClientApplicationContext.Current = clientContext;
#if DEBUG
                var message = request.ToString();
                Console.WriteLine("客户端发出消息:" + message);
#endif

                if (WcfLogManager.Current(channel.GetType()).MessageInfo.Client.Enabled)
                {
                    var direct = WcfLogManager.Current(channel.GetType()).MessageInfo.Client.Direction;
                    if (direct == WcfDirection.Both
                        || direct == WcfDirection.Send)
                    {
                        var log = WcfLogProvider.GetClientMessageInfo(
                        channelType.FullName,
                        ClientApplicationContext.Current.RequestIdentity,
                        "ClientMessageInspector.BeforeSendRequest",
                        MessageDirection.Send,
                        request.ToString());
                        WcfServiceLocator.GetLogService().LogWithoutException(log);
                    }
                }

                return channelType;
            }
            catch (Exception ex)
            {
                LocalLogService.Log(ex.ToString());
            }
            return channel.GetType();
        }
    }

很明显,通过IClientMessageInspector实现,Wcf的扩展可以总结为以下几个步骤:

1) 实现Wcf定义的一些接口

2) 把实现通过代码方式加入XX行为

3) 把XX行为通过代码方式或配置文件方式加入Wcf内部

那么这里的第二步实现如下:

internal class MessageInspectorEndpointBehavior : IEndpointBehavior
    {
        #region IEndpointBehavior Members

        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(new ClientMessageInspector());
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new ServerMessageInspector());
        }

        public void Validate(ServiceEndpoint endpoint)
        {
        }

        #endregion
    }

第三步的实现之前已经看到过了,在创建信道工厂的时候直接加入进去的。再来看一下ClientMessageInspector的实现,有几个需要注意的地方:

1) 通过WcfLogManager来读取各种日志是否要记录的开关,然后通过WcfLogProvider来获取各种日志,最后通过WcfServiceLocator.GetLogService()来获取日志服务记录日志。

2) 在这里为了性能,为契约对应的版本号做了缓存。这里要说明一点,由于这些横切日志部分是贯穿框架内部的,贯穿每一次方法调用的,所以一要确保高性能,二要确保不出错,不能为了一些日志而大幅降低服务的处理性能,甚至影响正常服务的运行。

3) 通过correlationState来返回契约类型,因为在AfterReceiveReply中很难提取契约类型。

再来说说服务端的部分,服务端的方法调用日志是通过IOperationInvoker实现的,具体实现不给出了。

服务端的消息日志很明显也是通过和IClientMessageInspector对应的IDispatchMessageInspector来实现的。

最后,服务端的异常是通过IErrorHandler来实现的:

internal class ErrorHandler : IErrorHandler
    {
        public bool HandleError(Exception error)
        {
            try
            {
#if DEBUG
                Console.WriteLine("服务端出现异常! Message:{0}, id : {1}", error.Message, error.Data["id"]);
#endif
                if (WcfLogManager.Current().ExceptionInfo.Server.Enabled)
                {
                    var exceptionID = error.Data["id"].ToString();

                    var log = WcfLogProvider.GetServerExceptionInfo("ErrorHandler.HandleErrer", error);
                    log.ID = exceptionID;
                    WcfServiceLocator.GetLogService().LogWithoutException(log);
                }

            }
            catch (Exception ex)
            {
                LocalLogService.Log(ex.ToString());
            }
            return true;
        }

        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            try
            {
                var errorid = Guid.NewGuid().ToString();
                error.Data.Add("id", errorid);
                ServerApplicationContext.Current.ServerExceptionID = errorid;
                FaultException fe = new FaultException(new FaultReason(error.Message));
                MessageFault messagefault = fe.CreateMessageFault();
                fault = Message.CreateMessage(version, messagefault, "http://www.5173.com");
            }
            catch (Exception ex)
            {
                LocalLogService.Log(ex.ToString());
            }
        }
    }

这里注意两点:

1) ProvideFault的时候我们会把异常包装为一个FaultException提供给客户端。

2) 会把服务端异常的ID在消息头中传给客户端,这个异常ID就是服务端记录的异常的ID。

我们可能还注意到,客户端需要传给服务端RequestIdentity,服务端需要传给客户端异常ID,客户端需要传给服务端客户端契约的版本和客户端的机器名,而服务端也需要把自己的机器名和契约版本传给客户端。这都是通过定义上下文类,在消息头中传递进行的:

1) 在IClientMessageInspector中,发送消息之前把服务端需要的上下文准备好,加入头传过去,在收到服务端返回的消息之后建立客户端的上下文。

2) 在IDispatchMessageInspector中,和1)反过来,收到消息之后建立服务端上下文,发回复之前把客户端需要的上下文数据准备好加入头中。
通过这些丰富的日志信息,我们可以:

1) 知道服务的启动信息,知道哪些客户端使用了我们的服务端

2) 知道客户端调用的信息,知道服务端执行方法的信息,知道两端的版本是否匹配,可以统计客户端和服务端的机器,知道方法的执行时间

3) 客户端通过异常可以关联服务端异常,服务端异常记录了详细的出错堆栈

4) 如果需要进一步跟踪问题可以开启消息记录(消耗一定的性能)

5) 通过上下文把客户端和服务端的调用变为一个整体

6) 由于都记录了机器名或IP地址,可以统计出有性能问题的机器,遇到问题也可以马上定位出错的机器,便于负载均衡环境定位问题

对于分布式的服务来说,服务端可能又会是其他服务的客户端,错综复杂,横切关注点尤其重要,因为这个框架很大一部分在实现这个。

当然,日志仅仅是存下来还不够,还需要有一个强大的查看以及统计后台,用于定位问题发现问题。

作者: lovecindywang
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
345 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
2月前
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
333 3
|
4月前
|
存储 监控 Serverless
阿里泛日志设计与实践问题之Grafana Loki在日志查询方案中存在哪些设计限制,如何解决
阿里泛日志设计与实践问题之Grafana Loki在日志查询方案中存在哪些设计限制,如何解决
|
1月前
|
消息中间件 存储 监控
微服务日志监控的挑战及应对方案
【10月更文挑战第23天】微服务化带来模块独立与快速扩展,但也使得日志监控复杂。日志作用包括业务记录、异常追踪和性能定位。
|
2月前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
139 1
|
3月前
|
Kubernetes API Docker
跟着iLogtail学习容器运行时与K8s下日志采集方案
iLogtail 作为开源可观测数据采集器,对 Kubernetes 环境下日志采集有着非常好的支持,本文跟随 iLogtail 的脚步,了解容器运行时与 K8s 下日志数据采集原理。
|
3月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
56 2
|
2月前
|
SQL XML 监控
SpringBoot框架日志详解
本文详细介绍了日志系统的重要性及其在不同环境下的配置方法。日志用于记录系统运行时的问题,确保服务的可靠性。文章解释了各种日志级别(如 info、warn、error 等)的作用,并介绍了常用的日志框架如 SLF4J 和 Logback。此外,还说明了如何在 SpringBoot 中配置日志输出路径及日志级别,包括控制台输出与文件输出的具体设置方法。通过这些配置,开发者能够更好地管理和调试应用程序。
|
3月前
|
Java
日志框架log4j打印异常堆栈信息携带traceId,方便接口异常排查
日常项目运行日志,异常栈打印是不带traceId,导致排查问题查找异常栈很麻烦。
|
3月前
|
运维 NoSQL Java
SpringBoot接入轻量级分布式日志框架GrayLog技术分享
在当今的软件开发环境中,日志管理扮演着至关重要的角色,尤其是在微服务架构下,分布式日志的统一收集、分析和展示成为了开发者和运维人员必须面对的问题。GrayLog作为一个轻量级的分布式日志框架,以其简洁、高效和易部署的特性,逐渐受到广大开发者的青睐。本文将详细介绍如何在SpringBoot项目中接入GrayLog,以实现日志的集中管理和分析。
297 1