提交 2501c566 编写于 作者: A ascrutae

1.将Context修改成List实现

2. 将RPC Client的序号作为 RPC Server的父级序号
3. 如果存在RPC调用包含其他调用,其他调用的埋点信息将不发送
上级 d7fc6c62
package com.ai.cloud.skywalking.buriedpoint;
import static com.ai.cloud.skywalking.conf.Config.BuriedPoint.EXCLUSIVE_EXCEPTIONS;
import java.util.HashSet;
import java.util.Set;
import com.ai.cloud.skywalking.api.IExceptionHandler;
import com.ai.cloud.skywalking.conf.Config;
import com.ai.cloud.skywalking.context.Context;
import com.ai.cloud.skywalking.logging.LogManager;
import com.ai.cloud.skywalking.logging.Logger;
import com.ai.cloud.skywalking.protocol.Span;
public class ApplicationExceptionHandler implements IExceptionHandler {
private static Logger logger = LogManager
.getLogger(ApplicationExceptionHandler.class);
private static String EXCEPTION_SPLIT = ",";
private static Set<String> exclusiveExceptionSet = null;
@Override
public void handleException(Throwable th) {
try {
if (exclusiveExceptionSet == null) {
Set<String> exclusiveExceptions = new HashSet<String>();
String[] exceptions = EXCLUSIVE_EXCEPTIONS
.split(EXCEPTION_SPLIT);
for (String exception : exceptions) {
exclusiveExceptions.add(exception);
}
exclusiveExceptionSet = exclusiveExceptions;
}
Span span = Context.getLastSpan();
span.handleException(th, exclusiveExceptionSet,
Config.BuriedPoint.MAX_EXCEPTION_STACK_LENGTH);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
}
package com.ai.cloud.skywalking.buriedpoint;
import com.ai.cloud.skywalking.buffer.ContextBuffer;
import com.ai.cloud.skywalking.conf.AuthDesc;
import com.ai.cloud.skywalking.conf.Config;
import com.ai.cloud.skywalking.context.Context;
import com.ai.cloud.skywalking.logging.LogManager;
import com.ai.cloud.skywalking.logging.Logger;
import com.ai.cloud.skywalking.model.ContextData;
import com.ai.cloud.skywalking.protocol.Span;
import java.util.HashSet;
import java.util.Set;
import static com.ai.cloud.skywalking.conf.Config.BuriedPoint.EXCLUSIVE_EXCEPTIONS;
public class BuriedPointInvoker {
private static Logger logger = LogManager
.getLogger(BuriedPointInvoker.class);
private static String EXCEPTION_SPLIT = ",";
private static Set<String> exclusiveExceptionSet = null;
public ContextData beforeInvoker(Span spanData) {
if (Config.BuriedPoint.PRINTF) {
logger.debug("TraceId:" + spanData.getTraceId()
+ "\tviewpointId:" + spanData.getViewPointId()
+ "\tParentLevelId:" + spanData.getParentLevel()
+ "\tLevelId:" + spanData.getLevelId());
}
// 将新创建的Context存放到ThreadLocal栈中。
Context.append(spanData);
// 并将当前的Context返回回去
return new ContextData(spanData);
}
public void afterInvoker() {
try {
if (!AuthDesc.isAuth())
return;
// 弹出上下文的栈顶中的元素
Span spanData = Context.removeLastSpan();
if (spanData == null || spanData.isInvalidate()) {
return;
}
// 加上花费时间
spanData.setCost(System.currentTimeMillis()
- spanData.getStartDate());
if (Config.BuriedPoint.PRINTF) {
logger.debug("TraceId:" + spanData.getTraceId()
+ "\tviewpointId:" + spanData.getViewPointId()
+ "\tParentLevelId:" + spanData.getParentLevel()
+ "\tLevelId:" + spanData.getLevelId()
+ "\tbusinessKey:" + spanData.getBusinessKey());
}
ContextBuffer.save(spanData);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
public void handleException(Throwable th) {
try {
if (exclusiveExceptionSet == null) {
Set<String> exclusiveExceptions = new HashSet<String>();
String[] exceptions = EXCLUSIVE_EXCEPTIONS
.split(EXCEPTION_SPLIT);
for (String exception : exceptions) {
exclusiveExceptions.add(exception);
}
exclusiveExceptionSet = exclusiveExceptions;
}
Span span = Context.getLastSpan();
span.handleException(th, exclusiveExceptionSet,
Config.BuriedPoint.MAX_EXCEPTION_STACK_LENGTH);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
}
package com.ai.cloud.skywalking.buriedpoint;
import com.ai.cloud.skywalking.api.IBuriedPointSender;
import com.ai.cloud.skywalking.buffer.ContextBuffer;
import com.ai.cloud.skywalking.conf.AuthDesc;
import com.ai.cloud.skywalking.conf.Config;
import com.ai.cloud.skywalking.context.Context;
import com.ai.cloud.skywalking.logging.LogManager;
import com.ai.cloud.skywalking.logging.Logger;
import com.ai.cloud.skywalking.model.ContextData;
......@@ -13,7 +10,7 @@ import com.ai.cloud.skywalking.model.Identification;
import com.ai.cloud.skywalking.protocol.Span;
import com.ai.cloud.skywalking.util.ContextGenerator;
public class LocalBuriedPointSender extends ApplicationExceptionHandler
public class LocalBuriedPointSender extends BuriedPointInvoker
implements IBuriedPointSender {
private static Logger logger = LogManager
......@@ -25,10 +22,8 @@ public class LocalBuriedPointSender extends ApplicationExceptionHandler
return new EmptyContextData();
Span spanData = ContextGenerator.generateSpanFromThreadLocal(id);
// 将新创建的Context存放到ThreadLocal栈中。
Context.append(spanData);
// 并将当前的Context返回回去
return new ContextData(spanData);
return super.beforeInvoker(spanData);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
return new EmptyContextData();
......@@ -36,31 +31,6 @@ public class LocalBuriedPointSender extends ApplicationExceptionHandler
}
public void afterSend() {
try {
if (!AuthDesc.isAuth())
return;
// 弹出上下文的栈顶中的元素
Span spanData = Context.removeLastSpan();
if (spanData == null) {
return;
}
// 加上花费时间
spanData.setCost(System.currentTimeMillis()
- spanData.getStartDate());
if (Config.BuriedPoint.PRINTF) {
logger.debug("TraceId:" + spanData.getTraceId()
+ "\tviewpointId:" + spanData.getViewPointId()
+ "\tParentLevelId:" + spanData.getParentLevel()
+ "\tLevelId:" + spanData.getLevelId()
+ "\tbusinessKey:" + spanData.getBusinessKey());
}
ContextBuffer.save(spanData);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
super.afterInvoker();
}
}
package com.ai.cloud.skywalking.buriedpoint;
import com.ai.cloud.skywalking.api.IBuriedPointReceiver;
import com.ai.cloud.skywalking.buffer.ContextBuffer;
import com.ai.cloud.skywalking.conf.AuthDesc;
import com.ai.cloud.skywalking.conf.Config;
import com.ai.cloud.skywalking.context.Context;
import com.ai.cloud.skywalking.logging.LogManager;
import com.ai.cloud.skywalking.logging.Logger;
import com.ai.cloud.skywalking.model.ContextData;
import com.ai.cloud.skywalking.model.Identification;
import com.ai.cloud.skywalking.protocol.Span;
import com.ai.cloud.skywalking.protocol.SpanType;
import com.ai.cloud.skywalking.util.ContextGenerator;
public class RPCBuriedPointReceiver extends ApplicationExceptionHandler
public class RPCBuriedPointReceiver extends BuriedPointInvoker
implements IBuriedPointReceiver {
private static Logger logger = LogManager
.getLogger(RPCBuriedPointReceiver.class);
public void afterReceived() {
try {
if (!AuthDesc.isAuth())
return;
// 获取上下文的栈顶中的元素
Span spanData = Context.removeLastSpan();
// 填上必要信息
spanData.setCost(System.currentTimeMillis()
- spanData.getStartDate());
// 存放到本地发送进程中
ContextBuffer.save(spanData);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
super.afterInvoker();
}
public void beforeReceived(ContextData context, Identification id) {
......@@ -43,18 +29,19 @@ public class RPCBuriedPointReceiver extends ApplicationExceptionHandler
Span spanData = ContextGenerator.generateSpanFromContextData(
context, id);
// 设置是否为接收端
spanData.setReceiver(true);
spanData.setSpanType(SpanType.RPC_SERVER);
if (Config.BuriedPoint.PRINTF) {
logger.debug("TraceId:" + spanData.getTraceId()
+ "\tviewpointId:" + spanData.getViewPointId()
+ "\tParentLevelId:" + spanData.getParentLevel()
+ "\tLevelId:" + spanData.getLevelId());
}
invalidateAllSpanIfIsNotFirstSpan(spanData);
Context.append(spanData);
super.beforeInvoker(spanData);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
private void invalidateAllSpanIfIsNotFirstSpan(Span spanData) {
if (!Context.getLastSpan().getTraceId().equals(spanData.getTraceId())) {
Context.invalidateAllSpan();
}
}
}
package com.ai.cloud.skywalking.buriedpoint;
public class RPCBuriedPointSender extends LocalBuriedPointSender {}
import com.ai.cloud.skywalking.api.IBuriedPointSender;
import com.ai.cloud.skywalking.conf.AuthDesc;
import com.ai.cloud.skywalking.context.Context;
import com.ai.cloud.skywalking.logging.LogManager;
import com.ai.cloud.skywalking.logging.Logger;
import com.ai.cloud.skywalking.model.ContextData;
import com.ai.cloud.skywalking.model.EmptyContextData;
import com.ai.cloud.skywalking.model.Identification;
import com.ai.cloud.skywalking.protocol.Span;
import com.ai.cloud.skywalking.protocol.SpanType;
import com.ai.cloud.skywalking.util.ContextGenerator;
public class RPCBuriedPointSender extends BuriedPointInvoker implements IBuriedPointSender {
private static Logger logger = LogManager
.getLogger(RPCBuriedPointSender.class);
@Override
public ContextData beforeSend(Identification id) {
try {
if (!AuthDesc.isAuth())
return new EmptyContextData();
Span spanData = ContextGenerator.generateSpanFromThreadLocal(id);
//设置SpanType的类型
spanData.setSpanType(SpanType.RPC_CLIENT);
Context.append(spanData);
return new ContextData(spanData.getTraceId(), generateSubParentLevelId(spanData), spanData.getCallType());
} catch (Throwable t) {
logger.error(t.getMessage(), t);
return new EmptyContextData();
}
}
private String generateSubParentLevelId(Span spanData) {
if (spanData.getParentLevel() == null) {
return spanData.getLevelId() + "";
}
return spanData.getParentLevel() + "." + spanData.getLevelId();
}
@Override
public void afterSend() {
super.afterInvoker();
}
}
package com.ai.cloud.skywalking.buriedpoint;
import com.ai.cloud.skywalking.api.IBuriedPointSender;
import com.ai.cloud.skywalking.buffer.ContextBuffer;
import com.ai.cloud.skywalking.conf.AuthDesc;
import com.ai.cloud.skywalking.conf.Config;
import com.ai.cloud.skywalking.context.Context;
......@@ -18,10 +17,9 @@ import com.ai.cloud.skywalking.util.TraceIdGenerator;
* 暂不确定多线程的实现方式
*
* @author wusheng
*
*/
@Deprecated
public class ThreadBuriedPointSender extends ApplicationExceptionHandler
public class ThreadBuriedPointSender extends BuriedPointInvoker
implements IBuriedPointSender {
private static Logger logger = LogManager
.getLogger(ThreadBuriedPointSender.class);
......@@ -65,18 +63,7 @@ public class ThreadBuriedPointSender extends ApplicationExceptionHandler
}
public void afterSend() {
Span span = Context.removeLastSpan();
if (span == null) {
return;
}
// 填上必要信息
span.setCost(System.currentTimeMillis() - span.getStartDate());
if (Config.BuriedPoint.PRINTF) {
logger.debug("viewpointId:" + span.getViewPointId()
+ "\tParentLevelId:" + span.getParentLevel() + "\tLevelId:"
+ span.getLevelId());
}
ContextBuffer.save(span);
super.afterInvoker();
}
}
package com.ai.cloud.skywalking.buriedpoint;
import com.ai.cloud.skywalking.api.IBuriedPointSender;
import com.ai.cloud.skywalking.buffer.ContextBuffer;
import com.ai.cloud.skywalking.conf.AuthDesc;
import com.ai.cloud.skywalking.conf.Config;
import com.ai.cloud.skywalking.context.Context;
import com.ai.cloud.skywalking.logging.LogManager;
import com.ai.cloud.skywalking.logging.Logger;
......@@ -17,10 +15,9 @@ import com.ai.cloud.skywalking.util.ContextGenerator;
* 暂不确定多线程的实现方式
*
* @author wusheng
*
*/
@Deprecated
public class ThreadFactoryBuriedPointSender extends ApplicationExceptionHandler
public class ThreadFactoryBuriedPointSender extends BuriedPointInvoker
implements IBuriedPointSender {
private static Logger logger = LogManager
.getLogger(ThreadBuriedPointSender.class);
......@@ -37,21 +34,6 @@ public class ThreadFactoryBuriedPointSender extends ApplicationExceptionHandler
}
public void afterSend() {
if (!AuthDesc.isAuth())
return;
// 获取上下文的栈顶中的元素
Span spanData = Context.removeLastSpan();
if (spanData == null) {
return;
}
// 填上必要信息
spanData.setCost(System.currentTimeMillis() - spanData.getStartDate());
if (Config.BuriedPoint.PRINTF) {
logger.debug("viewpointId:" + spanData.getViewPointId()
+ "\tParentLevelId:" + spanData.getParentLevel()
+ "\tLevelId:" + spanData.getLevelId());
}
ContextBuffer.save(spanData);
super.afterInvoker();
}
}
......@@ -2,7 +2,8 @@ package com.ai.cloud.skywalking.context;
import com.ai.cloud.skywalking.protocol.Span;
import java.util.Stack;
import java.util.ArrayList;
import java.util.List;
public class Context {
private static ThreadLocal<SpanNodeStack> nodes = new ThreadLocal<SpanNodeStack>();
......@@ -32,22 +33,30 @@ public class Context {
return nodes.get().pop();
}
public static void invalidateAllSpan() {
if (nodes.get() == null) {
nodes.set(new SpanNodeStack());
}
nodes.get().invalidateAllCurrentSpan();
}
static class SpanNodeStack {
private Stack<SpanNode> spans = new Stack<SpanNode>();
private List<SpanNode> spans = new ArrayList<SpanNode>();
public Span pop() {
Span span = spans.pop().getData();
Span span = listPop();
if (!isEmpty()) {
spans.peek().incrementNextSubSpanLevelId();
listPeek().incrementNextSubSpanLevelId();
}
return span;
}
public void push(Span span) {
if (!isEmpty()) {
spans.push(new SpanNode(span, spans.peek().getNextSubSpanLevelId()));
listPush(new SpanNode(span, listPeek().getNextSubSpanLevelId()));
} else {
spans.push(new SpanNode(span));
listPush(new SpanNode(span));
}
}
......@@ -56,12 +65,30 @@ public class Context {
if (spans.isEmpty()) {
return null;
}
return spans.peek().getData();
return listPeek().getData();
}
public boolean isEmpty() {
return spans.isEmpty();
}
private Span listPop() {
return spans.remove(spans.size() - 1).getData();
}
private SpanNode listPeek() {
return spans.get(spans.size() - 1);
}
private void listPush(SpanNode spanNode) {
spans.add(spans.size(), spanNode);
}
public void invalidateAllCurrentSpan() {
for (SpanNode spanNode : spans) {
spanNode.getData().setIsInvalidate(true);
}
}
}
static class SpanNode {
......
......@@ -13,11 +13,17 @@ public class ContextData {
}
public ContextData(String traceId, String parentLevel, String spanType) {
this.traceId = traceId;
this.parentLevel = parentLevel;
this.spanType = spanType;
}
public ContextData(Span span) {
this.traceId = span.getTraceId();
this.parentLevel = span.getParentLevel();
this.levelId = span.getLevelId();
this.spanType = span.getSpanType();
this.spanType = span.getSpanTypeDesc();
}
public ContextData(String contextDataStr) {
......
......@@ -6,7 +6,7 @@ import com.ai.cloud.skywalking.util.StringUtil;
public class Identification {
private String viewPoint;
private String businessKey;
private String spanType;
private String spanTypeDesc;
private String callType;
public Identification() {
......@@ -21,8 +21,8 @@ public class Identification {
return businessKey;
}
public String getSpanType() {
return spanType;
public String getSpanTypeDesc() {
return spanTypeDesc;
}
public String getCallType() {
......@@ -58,7 +58,7 @@ public class Identification {
if (StringUtil.isEmpty(spanType.getTypeName())) {
throw new IllegalArgumentException("Span Type name cannot be null");
}
sendData.spanType = spanType.getTypeName();
sendData.spanTypeDesc = spanType.getTypeName();
sendData.callType = spanType.getCallType().toString();
return this;
}
......
......@@ -5,6 +5,7 @@ import com.ai.cloud.skywalking.context.Context;
import com.ai.cloud.skywalking.model.ContextData;
import com.ai.cloud.skywalking.model.Identification;
import com.ai.cloud.skywalking.protocol.Span;
import com.ai.cloud.skywalking.protocol.SpanType;
public final class ContextGenerator {
/**
......@@ -42,7 +43,7 @@ public final class ContextGenerator {
}
private static void initNewSpanData(Span spanData, Identification id) {
spanData.setSpanType(id.getSpanType());
spanData.setSpanTypeDesc(id.getSpanTypeDesc());
spanData.setViewPointId(id.getViewPoint());
spanData.setBusinessKey(id.getBusinessKey());
//FIX Add Call Type field
......@@ -62,10 +63,20 @@ public final class ContextGenerator {
// 不存在,新创建一个Context
span = new Span(TraceIdGenerator.generate(), Config.SkyWalking.APPLICATION_CODE, Config.SkyWalking.USER_ID);
} else {
// 根据ParentContextData的TraceId和RPCID
// LevelId是由SpanNode类的nextSubSpanLevelId字段进行初始化的.
// 所以在这里不需要初始化
span = new Span(parentSpan.getTraceId(), Config.SkyWalking.APPLICATION_CODE, Config.SkyWalking.USER_ID);
// check parent span is RPC span
// if true, current span is invalidate and current span also belong to RPC span
if (parentSpan.isRPCClientSpan()) {
span.setSpanType(SpanType.RPC_CLIENT);
span.setIsInvalidate(true);
}
if (!StringUtil.isEmpty(parentSpan.getParentLevel())) {
span.setParentLevel(parentSpan.getParentLevel() + "." + parentSpan.getLevelId());
} else {
......
package test.ai.cloud.list;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
/**
* Created by xin on 16-7-2.
*/
public class ArrayListTest {
private List<String> data = new ArrayList<>();
@Before
public void initData() {
data.add("AAAA");
data.add("AAAAB");
data.add("AAAAB");
data.add("AAAAB");
}
@Test
public void testPop() {
data.remove(data.size() - 1);
assertEquals(data.size(), 3);
}
@Test
public void testPush() {
data.add(data.size(), "BBBBB");
assertEquals(data.get(data.size() - 1), "BBBBB");
}
}
package com.ai.cloud.skywalking.exception;
public class SpanTypeCannotConvertException extends RuntimeException {
public SpanTypeCannotConvertException(String spanTypeValue) {
super("Can not convert SpanTypeValue[" + spanTypeValue + "]");
}
}
......@@ -70,10 +70,10 @@ public class Span extends SpanData {
NEW_LINE_PLACEHOLDER, OS_NEW_LINE);
break;
case 9:
spanType = fieldValues[9];
spanTypeDesc = fieldValues[9];
break;
case 10:
isReceiver = Boolean.valueOf(fieldValues[10]);
spanType = SpanType.convert(fieldValues[10]);
break;
case 11:
businessKey = fieldValues[11].trim().replaceAll(
......@@ -115,7 +115,7 @@ public class Span extends SpanData {
toStringValue.append(levelId + SPAN_FIELD_SEPARATOR);
if (isNonBlank(viewPointId)) {
toStringValue.append(viewPointId + SPAN_FIELD_SEPARATOR);
toStringValue.append(generateViewPointBySpanType() + SPAN_FIELD_SEPARATOR);
} else {
toStringValue.append(" " + SPAN_FIELD_SEPARATOR);
}
......@@ -143,8 +143,8 @@ public class Span extends SpanData {
toStringValue.append(" " + SPAN_FIELD_SEPARATOR);
}
toStringValue.append(spanType + SPAN_FIELD_SEPARATOR);
toStringValue.append(isReceiver + SPAN_FIELD_SEPARATOR);
toStringValue.append(spanTypeDesc + SPAN_FIELD_SEPARATOR);
toStringValue.append(spanType.getValue() + SPAN_FIELD_SEPARATOR);
if (isNonBlank(businessKey)) {
// 换行符在各个系统中表现不一致,
......@@ -181,6 +181,16 @@ public class Span extends SpanData {
return toStringValue.toString();
}
private String generateViewPointBySpanType() {
if (spanType == SpanType.RPC_CLIENT) {
viewPointId = "RPC Client : " + viewPointId;
} else if (spanType == SpanType.RPC_SERVER) {
viewPointId = "RPC Server : " + viewPointId;
}
return viewPointId;
}
protected boolean isNonBlank(String str) {
return str != null && str.length() > 0;
}
......@@ -209,7 +219,7 @@ public class Span extends SpanData {
}
int sublength = maxExceptionStackLength;
if (maxExceptionStackLength > expMessage.length()){
if (maxExceptionStackLength > expMessage.length()) {
sublength = expMessage.length();
}
......@@ -220,4 +230,15 @@ public class Span extends SpanData {
}
}
public boolean isRPCClientSpan() {
if (this.spanType == SpanType.RPC_CLIENT) {
return true;
} else {
return false;
}
}
public void setIsInvalidate(boolean isInvalidate) {
this.isInvalidate = isInvalidate;
}
}
......@@ -67,17 +67,24 @@ public abstract class SpanData {
* 已字符串的形式描述<br/>
* 如:java,dubbo等
*/
protected String spanType = "";
protected String spanTypeDesc = "";
/**
* 节点调用类型描述<br/>
* @see com.ai.cloud.skywalking.protocol.CallType
*/
protected String callType = "";
/**
* 节点的状态<br/>
* 不参与序列化
*/
protected boolean isInvalidate = false;
/**
* 节点分布式类型<br/>
* 服务端/客户端
* 本地调用 / RPC服务端 / RPC客户端
*/
protected boolean isReceiver = false;
protected SpanType spanType = SpanType.LOCAL;
/**
* 节点调用过程中的业务字段<br/>
* 如:业务系统设置的订单号,SQL语句等
......@@ -147,20 +154,24 @@ public abstract class SpanData {
this.address = address;
}
public String getSpanType() {
return spanType;
public String getSpanTypeDesc() {
return spanTypeDesc;
}
public void setSpanType(String spanType) {
this.spanType = spanType;
public void setSpanTypeDesc(String spanTypeDesc) {
this.spanTypeDesc = spanTypeDesc;
}
public boolean isReceiver() {
return isReceiver;
public SpanType getSpanType() {
return spanType;
}
public void setSpanType(SpanType spanType) {
this.spanType = spanType;
}
public void setReceiver(boolean receiver) {
isReceiver = receiver;
public boolean isInvalidate() {
return isInvalidate;
}
public void setBusinessKey(String businessKey) {
......
package com.ai.cloud.skywalking.protocol;
import com.ai.cloud.skywalking.exception.SpanTypeCannotConvertException;
/**
* Created by xin on 16-7-2.
*/
public enum SpanType {
LOCAL((byte) 1), RPC_CLIENT((byte) 2), RPC_SERVER((byte) 4);
private byte value;
SpanType(byte value) {
this.value = value;
}
static SpanType convert(String spanTypeValue) {
switch (Byte.valueOf(spanTypeValue)){
case 1 : return LOCAL;
case 2 : return RPC_CLIENT;
case 3 : return RPC_SERVER;
default:
throw new SpanTypeCannotConvertException(spanTypeValue);
}
}
public byte getValue() {
return value;
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册