Skip to content

Commit

Permalink
feature: support monitoring nginx metrics and add a help doc (#1420)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-little-fool authored Dec 14, 2023
1 parent 03fec2c commit 405b882
Show file tree
Hide file tree
Showing 8 changed files with 905 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
package org.dromara.hertzbeat.collector.collect.nginx;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.dromara.hertzbeat.collector.collect.AbstractCollect;
import org.dromara.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.dromara.hertzbeat.collector.dispatch.DispatchConstants;
import org.dromara.hertzbeat.collector.util.CollectUtil;
import org.dromara.hertzbeat.common.constants.CollectorConstants;
import org.dromara.hertzbeat.common.constants.CommonConstants;
import org.dromara.hertzbeat.common.entity.job.Metrics;
import org.dromara.hertzbeat.common.entity.job.protocol.NginxProtocol;
import org.dromara.hertzbeat.common.entity.message.CollectRep;
import org.dromara.hertzbeat.common.util.CommonUtil;
import org.dromara.hertzbeat.common.util.IpDomainUtil;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.dromara.hertzbeat.common.constants.SignConstants.RIGHT_DASH;

/**
* nginx collect
*
*/
@Slf4j
public class NginxCollectImpl extends AbstractCollect {

private final static int SUCCESS_CODE = 200;
private final static String NGINX_STATUS_NAME = "nginx_status";
private final static String REQ_STATUS_NAME = "req_status";
private final static String AVAILABLE = "available";
private final static String CONNECTIONS = "connections";
private final static String ACTIVE = "active";
private final static String GET = "get";
private final static String FIELD_SPLIT = "_";
private final static String REGEX_KEYS = "server\\s+(\\w+)\\s+(\\w+)\\s+(\\w+)";
private final static String REGEX_VALUES = "(\\d+) (\\d+) (\\d+)";
private final static String REGEX_SERVER = "(\\w+): (\\d+)";
private final static String REGEX_SPLIT = "\\r?\\n";
private final static String REGEX_LINE_SPLIT = "\\s+";


public NginxCollectImpl() {

}

@Override
public void collect(CollectRep.MetricsData.Builder builder, long monitorId, String app, Metrics metrics) {
long startTime = System.currentTimeMillis();

// 校验参数
try {
validateParams(metrics);
} catch (Exception e) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(e.getMessage());
return;
}

HttpContext httpContext = createHttpContext(metrics.getNginx());
HttpUriRequest request = createHttpRequest(metrics.getNginx());
try {
// 发起http请求,获取响应数据
CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != SUCCESS_CODE) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("StatusCode " + statusCode);
return;
}
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);

Long responseTime = System.currentTimeMillis() - startTime;
// 根据metrics name选择调用不同解析方法
if (NGINX_STATUS_NAME.equals(metrics.getName()) || AVAILABLE.equals(metrics.getName())) {
parseNginxStatusResponse(builder, resp, metrics, responseTime);
} else if (REQ_STATUS_NAME.equals(metrics.getName())) {
parseReqStatusResponse(builder, resp, metrics, responseTime);
}
} catch (IOException e1) {
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
log.info(errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (Exception e2) {
String errorMsg = CommonUtil.getMessageFromThrowable(e2);
log.info(errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}

}

@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_NGINX;
}

private void validateParams(Metrics metrics) throws Exception {
NginxProtocol nginxProtocol = metrics.getNginx();
if (metrics == null || nginxProtocol == null || nginxProtocol.isInValid()) {
throw new Exception("Nginx collect must has nginx params");
}
if (nginxProtocol.getUrl() == null
|| "".equals(nginxProtocol.getUrl())
|| !nginxProtocol.getUrl().startsWith(RIGHT_DASH)) {
nginxProtocol.setUrl(nginxProtocol.getUrl() == null ? RIGHT_DASH : RIGHT_DASH + nginxProtocol.getUrl().trim());
}
}

private HttpContext createHttpContext(NginxProtocol nginxProtocol) {
HttpHost host = new HttpHost(nginxProtocol.getHost(), Integer.parseInt(nginxProtocol.getPort()));
HttpClientContext httpClientContext = new HttpClientContext();
httpClientContext.setTargetHost(host);
return httpClientContext;
}

private HttpUriRequest createHttpRequest(NginxProtocol nginxProtocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
// uri
String uri = CollectUtil.replaceUriSpecialChar(nginxProtocol.getUrl());
if (IpDomainUtil.isHasSchema(nginxProtocol.getHost())) {
requestBuilder.setUri(nginxProtocol.getHost() + ":" + nginxProtocol.getPort() + uri);
} else {
String ipAddressType = IpDomainUtil.checkIpAddressType(nginxProtocol.getHost());
String baseUri = CollectorConstants.IPV6.equals(ipAddressType)
? String.format("[%s]:%s", nginxProtocol.getHost(), nginxProtocol.getPort() + uri)
: String.format("%s:%s", nginxProtocol.getHost(), nginxProtocol.getPort() + uri);

requestBuilder.setUri(CollectorConstants.HTTP_HEADER + baseUri);
}

requestBuilder.addHeader(HttpHeaders.CONNECTION, "keep-alive");
requestBuilder.addHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36");

requestBuilder.addHeader(HttpHeaders.ACCEPT, "text/plain");

Integer timeout = Integer.parseInt(nginxProtocol.getTimeout());
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
}

/**
* 解析nginx自带ngx_http_stub_status_module模块暴露信息
* @param builder
* @param resp
* @param metrics
* @param responseTime
*/
private void parseNginxStatusResponse(CollectRep.MetricsData.Builder builder, String resp, Metrics metrics,
Long responseTime) {
/** example
* Active connections: 2
* server accepts handled requests
* 4 4 2
* Reading: 0 Writing: 1 Waiting: 1
*/
List<String> aliasFields = metrics.getAliasFields();
Map<String,Object> metricMap = regexNginxStatusMatch(resp, metrics.getAliasFields().size());
// 返回数据
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliasFields) {
Object value = metricMap.get(alias);
if (value != null) {
valueRowBuilder.addColumns(String.valueOf(value));
} else {
if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumns(responseTime.toString());
} else {
valueRowBuilder.addColumns(CommonConstants.NULL_VALUE);
}
}
}
builder.addValues(valueRowBuilder.build());
}

/**
* 解析ngx_http_reqstat_module模块暴露信息
* @param builder
* @param resp
* @param metrics
* @param responseTime
*/
private void parseReqStatusResponse(CollectRep.MetricsData.Builder builder, String resp, Metrics metrics,
Long responseTime) {
/** example
* zone_name key max_active max_bw traffic requests active bandwidth
* imgstore_appid 43 27 6M 63G 374063 0 0
* imgstore_appid 53 329 87M 2058G 7870529 50 25M
* server_addr 10.128.1.17 2 8968 24M 1849 0 0
* server_addr 127.0.0.1 1 6M 5G 912 1 0
* server_addr 180.96.x.1 3358 934M 27550G 141277391 891 356M
* server_addr 180.96.x.2 78 45M 220G 400704 0 0
* server_addr 180.96.x.3 242 58M 646G 2990547 42 7M
* server_name d.123.sogou.com 478 115M 2850G 30218726 115 39M
* server_name dl.pinyin.sogou.com 913 312M 8930G 35345453 225 97M
* server_name download.ie.sogou.com 964 275M 7462G 7979817 297 135M
*/
List<ReqSatusResponse> reqSatusResponses = regexReqStatusMatch(resp);
List<String> aliasFields = metrics.getAliasFields();

for (ReqSatusResponse reqSatusResponse : reqSatusResponses) {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliasFields) {
if (CollectorConstants.RESPONSE_TIME.equals(alias)) {
valueRowBuilder.addColumns(String.valueOf(responseTime));
} else {
try {
String methodName = reqSatusResponse.getFieldMethodName(alias);
Object value = reflect(reqSatusResponse, methodName);
value = value == null ? CommonConstants.NULL_VALUE : value;
valueRowBuilder.addColumns(String.valueOf(value));
} catch (NoSuchMethodException e1) {
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
log.info(errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (InvocationTargetException e2) {
String errorMsg = CommonUtil.getMessageFromThrowable(e2);
log.info(errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (IllegalAccessException e3) {
String errorMsg = CommonUtil.getMessageFromThrowable(e3);
log.info(errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}
}
}
builder.addValues(valueRowBuilder.build());
}
}

private Object reflect(ReqSatusResponse reqSatusResponse, String methodName) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> clazz = reqSatusResponse.getClass();
Method method = clazz.getMethod(methodName);
return method.invoke(reqSatusResponse);
}

private Map<String,Object> regexNginxStatusMatch(String resp, Integer aliasFieldsSize) {
Map<String,Object> metricsMap = new HashMap<>(aliasFieldsSize);
// 正则提取监控信息
Pattern pattern = Pattern.compile(REGEX_SERVER);
Matcher matcher = pattern.matcher(resp);
while (matcher.find()) {
String key = StringUtils.lowerCase(matcher.group(1));
String value = matcher.group(2);
metricsMap.put(CONNECTIONS.equals(key) ? ACTIVE : key, value);
}
Pattern pattern1 = Pattern.compile(REGEX_KEYS);
Matcher matcher1 = pattern1.matcher(resp);
Pattern pattern2 = Pattern.compile(REGEX_VALUES);
Matcher matcher2 = pattern2.matcher(resp);
if (matcher1.find() && matcher2.find()) {
for (int i = 0; i < matcher1.groupCount(); i++) {
metricsMap.put(matcher1.group(i + 1), matcher2.group(i + 1));
}
}
return metricsMap;
}

private List<ReqSatusResponse> regexReqStatusMatch(String resp) {
List<ReqSatusResponse> reqSatusResponses = new ArrayList<>();

String[] lines = resp.split(REGEX_SPLIT);
for (int i = 1; i < lines.length; i++) {
String[] values = lines[i].split(REGEX_LINE_SPLIT);
ReqSatusResponse reqSatusResponse = ReqSatusResponse.builder()
.zoneName(values[0])
.key(values[1])
.maxActive(values[2])
.maxBw(values[3])
.traffic(values[4])
.requests(values[5])
.active(values[6])
.bandwidth(values[7])
.build();
reqSatusResponses.add(reqSatusResponse);
}
return reqSatusResponses;
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
static class ReqSatusResponse {
private String zoneName; // zone_name

private String maxActive; // max_active

private String key; // key

private String maxBw; // max_bw

private String traffic; // traffic

private String requests; // requests

private String active; // active

private String bandwidth; // bandwidth

public String getFieldMethodName(String name) {
String[] words = name.split(FIELD_SPLIT);
StringBuilder result = new StringBuilder();
for (String word : words) {
result.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
}
return GET + result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public interface DispatchConstants {
* protocol telnet
*/
String PROTOCOL_TELNET = "telnet";
/**
* protocol nginx
*/
String PROTOCOL_NGINX = "nginx";
/**
* protocol smtp
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ org.dromara.hertzbeat.collector.collect.mq.RocketmqSingleCollectImpl
org.dromara.hertzbeat.collector.collect.udp.UdpCollectImpl
org.dromara.hertzbeat.collector.collect.push.PushCollectImpl
org.dromara.hertzbeat.collector.collect.dns.DnsCollectImpl
org.dromara.hertzbeat.collector.collect.nginx.NginxCollectImpl
Loading

0 comments on commit 405b882

Please sign in to comment.