使用dubbo做分布式服务,当查看日志时,需要在多个应用中对日志进行查询;若一个接口被多个客户端同时调用,则会出现日志查找辨别非常困难,无法及时定位错误。
本示例基于MVC拦截器、Dubbo的Filter及SLF的MDC实现,原理为在客户端调用http接口时,利用MVC拦截器,在MDC中放置一个reqId,并在logback日志中对此reqId进行输出;当此次请求进行RPC请求时,Filter会获取MDC中的reqId,并将此reqId以隐藏参数的形式传递给服务提供者;服务提供者的Filter会再将隐藏参数中的reqId放到服务端调用的MDC中,从而实现整个调用流程使用同一个reqId;
1、服务端实现
1.1、源码目录
目录说明:
dubbo.trace.server.api包:此包为接口及接口实现;
dubbo.trace.server.config包:此包配置加载dubbo的配置;
dubbo.trace.server.filter包:服务端Filter实现;
resource/config目录:logback日志配置及dubbo配置;
resource/config/META-INF/dubbo目录:dubbo的SPI扩展目录,本处扩展了com.alibaba.dubbo.rpc.Filter接口;
1.2、接口及实现
接口:
public interface DubboTraceApi {
public String echoTest(String msg);
}
实现:
public class DubboTraceApiImpl implements DubboTraceApi {
private static Logger logger = LoggerFactory.getLogger(DubboTraceApiImpl.class);
@Override
public String echoTest(String msg){
logger.info("echoTest:{}", msg);
return msg;
}
}
1.3、Filter实现
public class ServerTraceFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(ServerTraceFilter.class);
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
logger.info("####className:{}", invocation.getClass().getName());
logger.info("####methodName:{}", invocation.getMethodName());
//获取appCode及secretKey
String appCode = RpcContext.getContext().getAttachment("appCode");
String secretKey = RpcContext.getContext().getAttachment("secretKey");
if(Strings.isNullOrEmpty(appCode) || Strings.isNullOrEmpty(secretKey)){
throw new RpcException("Sorry, your access is denied!");
}
logger.info("appCode:{}", appCode);
//获取reqId,若没有,则通过UUID生成一个;然后将reqId放到MDC中,便于日志中打印
String reqId = RpcContext.getContext().getAttachment("reqId");
reqId = !Strings.isNullOrEmpty(reqId) ? reqId : UUID.randomUUID().toString();
MDC.put("appCode", appCode);
MDC.put("reqId", reqId);
logger.info("reqId:{}", reqId);
if(!appCode.equals("zhaozhou11") || !secretKey.equals("666666")){
throw new RpcException("your appCode or secretKey is error!");
}
return invoker.invoke(invocation);
}
}
本处主要获取appCode、secretKey及reqId,对appCode及secretKey进行验证,将appCode及reqId放到MDC中;
1.4、dubbo服务端配置
dubbo-rpc-provider.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="privider-api-test" />
<!-- 使用zookeeper注册中心暴露服务地址 -->
<dubbo:registry id="zookeeperRegistry" protocol="zookeeper" address="zookeeper://127.0.0.1:2181" check="false" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:provider filter="traceFilter" ></dubbo:provider>
<!-- 声明需要暴露的服务接口 -->
<dubbo:service id="dubboTestApi" interface="dubbo.trace.server.api.DubboTraceApi" ref="dubboTraceApiImpl" cache="false" />
<!-- 和本地bean一样实现服务 -->
<bean id="dubboTraceApiImpl" class="dubbo.trace.server.api.DubboTraceApiImpl" />
</beans>
主要配置注册中心、暴露协议、服务提供者等,重点是设置provider的Filter为traceFilter,此处的traceFilter是在META-INF/dubbo/com.alibaba.dubbo.rpc.Filter文件中进行设置的;
设置如下:
traceFilter=dubbo.trace.server.filter.ServerTraceFilter
1.5、logback日志配置
logback日志配置就不全贴出来了,只贴出日志输出格式的设置,如下:
<property name="patternlayout" value="==%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L [%X{appCode}]-[%X{reqId}] - %msg%n"/>
格式中变量appCode及reqId即为在Filter中设置的MDC值。
2、客户端实现
2.1、源码目录
目录说明:
dubbo.trace.client.config包:客户端的bean配置加载,包括dubbo及接口拦截器;
dubbo.trace.client.controller包:controller类;
dubbo.trace.client.filter包:客户端的Filter实现;
resource/config目录:dubbo客户端配置及logback配置;
resource/META-INF/dubbo目录:dubbo的SPI扩展配置;
resource/templates目录:页面资源目录
2.2、拦截器实现
public class TraceClientInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
MDC.put("userName", "zhaozhou11");
MDC.put("reqId", UUID.randomUUID().toString());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
MDC.remove("userName");
MDC.remove("reqId");
}
}
拦截器主要是在接口调用之前设置MDC值,此处设置userName及reqId,并在调用完成后清除;
2.2、Filter实现
public class ConsumerRpcFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(ConsumerRpcFilter.class);
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
logger.info("###className:{}", invocation.getClass().getName());
logger.info("###methodName:{}", invocation.getMethodName());
String appCode = invoker.getUrl().getParameter("appCode");
String secretKey = invoker.getUrl().getParameter("secretKey");
String reqId = MDC.get("reqId");
reqId = Strings.isNotEmpty(reqId) ? reqId : UUID.randomUUID().toString();
RpcContext.getContext().setAttachment("appCode",appCode);
RpcContext.getContext().setAttachment("secretKey", secretKey);
RpcContext.getContext().setAttachment("reqId", reqId);
return invoker.invoke(invocation);
}
}
本处主要是获取appCode、secretKey及reqId,并将这三个参数放到RpcContext的Attachment中,而服务端就可以从其对应的Attachment中获取这些参数。
2.3、Controller实现
@Controller
@RequestMapping(value = "")
public class MainController {
@Autowired
private DubboTraceApi traceApi;
@RequestMapping(value = "/")
public String gotoIndexPage(Model model){
String ret = traceApi.echoTest("this is test!");
model.addAttribute("ret", ret);
return "index";
}
}
3、测试输出
确保zookeeper是启动的,并启动服务端和客户端,浏览器访问:http://localhost:8080/
客户端日志:
服务端日志:
服务端和客户端日志中都有相同的reqId。