druid-admin

Feature: add support for services in k8s cluster without a discovery service like eureka, nacos...
Fix: 1. fix fastjson error when serialize large object. 2. fix an int overflow error
This commit is contained in:
许朕 2024-05-09 15:33:45 +08:00 committed by LiZongbo
parent b713c5ae4e
commit 32f2e26fb6
9 changed files with 145 additions and 23 deletions

View File

@ -10,7 +10,7 @@
</parent>
<groupId>com.alibaba</groupId>
<artifactId>druid-admin</artifactId>
<version>1.2.12</version>
<version>1.2.22</version>
<name>druid-admin</name>
<description>Demo project for Spring Boot</description>
@ -66,7 +66,7 @@
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.12</version>
<version>1.2.22</version>
</dependency>
<dependency>
@ -83,6 +83,21 @@
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.kubernetes/client-java -->
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<version>16.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.14.9</version>
</dependency>
</dependencies>
<dependencyManagement>

View File

@ -30,6 +30,12 @@ public class DruidAdminApplication {
if (properties.getLoginPassword() != null) {
registrationBean.addInitParameter("loginPassword", properties.getLoginPassword());
}
if (properties.getKubeConfigFilePath() != null) {
registrationBean.addInitParameter("kubeConfigFilePath", properties.getKubeConfigFilePath());
}
if (properties.getK8sNamespace() != null) {
registrationBean.addInitParameter("k8sNamespace", properties.getK8sNamespace());
}
return registrationBean;
}

View File

@ -29,4 +29,14 @@ public class MonitorProperties {
* 访问路径
*/
private String contextPath;
/**
* k8s集群访问凭证
*/
private String kubeConfigFilePath;
/**
* k8s集群命名空间
*/
private String k8sNamespace;
}

View File

@ -118,7 +118,7 @@ public class DataSourceResult {
@JSONField(name = "RemoveAbandoned")
private boolean RemoveAbandoned;
@JSONField(name = "ClobOpenCount")
private int ClobOpenCount;
private long ClobOpenCount;
@JSONField(name = "BlobOpenCount")
private int BlobOpenCount;
@JSONField(name = "KeepAliveCheckCount")

View File

@ -0,0 +1,62 @@
package com.alibaba.druid.admin.service;
import com.alibaba.druid.admin.model.ServiceNode;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1Pod;
import io.kubernetes.client.openapi.models.V1PodList;
import io.kubernetes.client.openapi.models.V1Service;
import io.kubernetes.client.util.ClientBuilder;
import io.kubernetes.client.util.KubeConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
@Component
public class K8sDiscoveryClient {
public Map<String, ServiceNode> getK8sPodsInfo(List<String> serviceNames, String kubeConfigFilePath, String namespace) throws IOException, ApiException {
InputStream inputStream = new ClassPathResource(kubeConfigFilePath).getInputStream();
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
ApiClient client = ClientBuilder.kubeconfig(KubeConfig.loadKubeConfig(reader)).build();
Configuration.setDefaultApiClient(client);
CoreV1Api api = new CoreV1Api();
List<ServiceNode> serviceNodes = new ArrayList<>();
V1PodList podList = api.listNamespacedPod(namespace, null, null, null, null, null, null, null, null, null, null);
for (String serviceName : serviceNames) {
V1Service service = api.readNamespacedService(serviceName, namespace, null);
List<V1Pod> servicePods = podList.getItems().stream().filter(i -> Objects.requireNonNull(Objects.requireNonNull(i.getMetadata()).getName()).startsWith(serviceName)).collect(Collectors.toList());
for (V1Pod pod : servicePods) {
String podId = pod.getMetadata().getUid();
String podIp = pod.getStatus().getPodIP();
Integer port = Objects.requireNonNull(Objects.requireNonNull(service.getSpec()).getPorts()).stream().filter(i -> serviceName.equalsIgnoreCase(i.getName())).findFirst().get().getPort();
ServiceNode serviceNode = new ServiceNode();
serviceNode.setId(podId);
serviceNode.setPort(port);
serviceNode.setAddress(podIp);
serviceNode.setServiceName(serviceName);
serviceNodes.add(serviceNode);
MonitorStatService.serviceIdMap.put(podId, serviceNode);
log.info("pod info: " + serviceNode);
}
}
return serviceNodes.stream().collect(Collectors.toMap(i -> i.getServiceName() + "-" + i.getAddress() + "-" + i.getPort(),
Function.identity(), (v1, v2) -> v2));
}
}

View File

@ -2,12 +2,7 @@ package com.alibaba.druid.admin.service;
import com.alibaba.druid.admin.config.MonitorProperties;
import com.alibaba.druid.admin.model.ServiceNode;
import com.alibaba.druid.admin.model.dto.ConnectionResult;
import com.alibaba.druid.admin.model.dto.DataSourceResult;
import com.alibaba.druid.admin.model.dto.SqlDetailResult;
import com.alibaba.druid.admin.model.dto.SqlListResult;
import com.alibaba.druid.admin.model.dto.WallResult;
import com.alibaba.druid.admin.model.dto.WebResult;
import com.alibaba.druid.admin.model.dto.*;
import com.alibaba.druid.admin.util.HttpUtil;
import com.alibaba.druid.stat.DruidStatServiceMBean;
import com.alibaba.druid.support.http.stat.WebAppStatManager;
@ -18,18 +13,19 @@ import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import io.kubernetes.client.openapi.ApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -44,7 +40,7 @@ public class MonitorStatService implements DruidStatServiceMBean {
public static final int RESULT_CODE_ERROR = -1;
private static final int DEFAULT_PAGE = 1;
private static final int DEFAULT_PER_PAGE_COUNT = Integer.MAX_VALUE;
private static final int DEFAULT_PER_PAGE_COUNT = 1000;
private static final String ORDER_TYPE_DESC = "desc";
private static final String ORDER_TYPE_ASC = "asc";
private static final String DEFAULT_ORDER_TYPE = ORDER_TYPE_ASC;
@ -60,13 +56,25 @@ public class MonitorStatService implements DruidStatServiceMBean {
@Autowired
private MonitorProperties monitorProperties;
@Autowired
private K8sDiscoveryClient k8sDiscoveryClient;
/**
* 获取所有服务信息
*
* @return
*/
public Map<String, ServiceNode> getAllServiceNodeMap(){
public Map<String, ServiceNode> getAllServiceNodeMap() {
List<String> services = discoveryClient.getServices();
List<String> applications = monitorProperties.getApplications();
String kubeConfig = monitorProperties.getKubeConfigFilePath();
if (CollectionUtils.isEmpty(services) && !Strings.isNullOrEmpty(kubeConfig)) {
try {
return k8sDiscoveryClient.getK8sPodsInfo(applications, kubeConfig, monitorProperties.getK8sNamespace());
} catch (IOException | ApiException e) {
log.error("get k8s resource fail: ", e);
}
}
List<ServiceNode> serviceNodes = new ArrayList<>();
for (String service : services) {
List<ServiceInstance> instances = discoveryClient.getInstances(service);
@ -79,7 +87,7 @@ public class MonitorStatService implements DruidStatServiceMBean {
int port = instance.getPort();
String serviceId = instance.getServiceId();
// 根据前端参数采集指定的服务
if (monitorProperties.getApplications().contains(serviceId)) {
if (applications.contains(serviceId)) {
ServiceNode serviceNode = new ServiceNode();
serviceNode.setId(instanceId);
serviceNode.setPort(port);
@ -100,11 +108,18 @@ public class MonitorStatService implements DruidStatServiceMBean {
* @param parameters
* @return
*/
public Map<String, ServiceNode> getServiceAllNodeMap(Map<String, String> parameters){
public Map<String, ServiceNode> getServiceAllNodeMap(Map<String, String> parameters) {
String requestServiceName = parameters.get("serviceName");
List<String> services = discoveryClient.getServices();
String kubeConfig = monitorProperties.getKubeConfigFilePath();
if (CollectionUtils.isEmpty(services) && !Strings.isNullOrEmpty(kubeConfig)) {
try {
return k8sDiscoveryClient.getK8sPodsInfo(Lists.newArrayList(requestServiceName), kubeConfig, monitorProperties.getK8sNamespace());
} catch (IOException | ApiException e) {
log.error("get k8s resource fail: ", e);
}
}
List<ServiceNode> serviceNodes = new ArrayList<>();
for (String service : services) {
List<ServiceInstance> instances = discoveryClient.getInstances(service);
for (ServiceInstance instance : instances) {
@ -130,6 +145,7 @@ public class MonitorStatService implements DruidStatServiceMBean {
return serviceNodes.stream().collect(Collectors.toMap(i -> i.getServiceName() + "-" + i.getAddress() + "-" + i.getPort(),
Function.identity(), (v1, v2) -> v2));
}
@Override
public String service(String url) {
Map<String, String> parameters = getParameters(url);
@ -343,7 +359,13 @@ public class MonitorStatService implements DruidStatServiceMBean {
}
}
List<Map<String, Object>> maps = comparatorOrderBy(arrayMap, parameters);
String jsonString = JSON.toJSONString(maps);
String jsonString = JSON.toJSONString(
maps,
JSONWriter.Feature.LargeObject,
JSONWriter.Feature.ReferenceDetection,
JSONWriter.Feature.BrowserCompatible,
JSONWriter.Feature.BrowserSecure
);
JSONArray objects = JSON.parseArray(jsonString);
JSONObject jsonObject = new JSONObject();
jsonObject.put("ResultCode", RESULT_CODE_SUCCESS);
@ -351,7 +373,6 @@ public class MonitorStatService implements DruidStatServiceMBean {
return jsonObject.toJSONString();
}
/**
* 数据源监控
*
@ -368,6 +389,7 @@ public class MonitorStatService implements DruidStatServiceMBean {
String serviceName = serviceNode.getServiceName();
String url = "http://" + serviceNode.getAddress() + ":" + serviceNode.getPort() + "/druid/datasource.json";
log.info("request url: " + url);
DataSourceResult dataSourceResult = HttpUtil.get(url, lastResult.getClass());
if (dataSourceResult != null) {
List<DataSourceResult.ContentBean> nodeContent = dataSourceResult.getContent();

View File

@ -1,6 +1,7 @@
package com.alibaba.druid.admin.util;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
@ -15,9 +16,11 @@ import java.nio.charset.StandardCharsets;
* @author linchtech
* @date 2020-09-16 16:12
**/
@Slf4j
public class HttpUtil {
public static <T> T get(String url, Class<T> resultType) {
CloseableHttpClient httpClient = HttpClients.createDefault();
log.info(url);
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Content-type", "application/json");
CloseableHttpResponse response;

View File

@ -31,6 +31,10 @@ eureka:
monitor:
applications: #需要监控的服务名spring.application.name
- a
- b
login-username: admin #监控页面的登录用户名和密码
login-password: 123456
kube-config-file-path: # k8s集群访问凭证相对路径默认resources目录
k8s-namespace: # k8s集群命名空间

View File

@ -8,7 +8,7 @@ druid.common = function () {
// only one page for now
var sqlViewPage = 1;
var sqlViewPerPageCount = 1000000;
var sqlViewPerPageCount = 100;
return {
init: function () {