Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 71 additions & 5 deletions docs/api-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1652,17 +1652,83 @@ POST /api/metrics/query

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `metric` | `string` | 是 | 指标名称(如 `tps_in`、`tps_out`、`message_count`、`disk_usage`) |
| `metric` | `string` | 是 | PromQL 表达式,最大 4096 个字符 |
| `start` | `number` | 是 | 起始时间(Unix 时间戳,秒) |
| `end` | `number` | 是 | 结束时间(Unix 时间戳,秒) |
| `step` | `string` | | 采样步长(如 `"60s"`、`"5m"`、`"1h"`) |
| `step` | `string` | | 查询分辨率,可以是持续时间或秒数(如 `"30s"`、`"5m"`、`"1h"`) |

**Response `data`:** `MetricsResult`
**Request 示例:**

```json
{
"metric": "sum(rate(rocketmq_messages_in_total[1m])) by (node_id)",
"start": 1784112606,
"end": 1784114406,
"step": "30s"
}
```

**Response `data`:** `MetricData`

| 字段 | 类型 | 说明 |
|------|------|------|
| `resultType` | `string` | Prometheus 结果类型;范围查询通常为 `matrix` |
| `series` | `MetricSeries[]` | 查询返回的时间序列 |
| `warnings` | `string[]` | Prometheus 返回的非致命警告,没有警告时为空数组 |

**MetricSeries:**

| 字段 | 类型 | 说明 |
|------|------|------|
| `labels` | `object` | 序列的完整标签集合,包括可能存在的 `__name__` |
| `values` | `MetricSample[]` | 浮点样本;没有浮点样本时为空数组 |
| `histograms` | `MetricHistogramSample[]` | Native Histogram 样本;没有 Histogram 样本时为空数组 |

同一序列可能只有 `values`、只有 `histograms`,或同时包含两者。

**MetricSample:**

| 字段 | 类型 | 说明 |
|------|------|------|
| `timestamp` | `number` | Unix 时间戳,可能包含小数秒 |
| `value` | `string` | Prometheus 样本原始字符串,保留小数精度以及 `NaN`、`+Inf`、`-Inf` |

**MetricHistogramSample:**

| 字段 | 类型 | 说明 |
|------|------|------|
| `metric` | `string` | 指标名称 |
| `values` | `[number, number][]` | 数据点数组,每项为 `[timestamp, value]` |
| `timestamp` | `number` | Unix 时间戳,可能包含小数秒 |
| `histogram` | `object` | Prometheus Native Histogram 原始对象,包含 `count`、`sum` 和 `buckets` |

**Prometheus 配置:**

```yaml
studio:
metrics:
prometheus:
base-url: ${STUDIO_METRICS_PROMETHEUS_BASE_URL:}
connect-timeout: ${STUDIO_METRICS_PROMETHEUS_CONNECT_TIMEOUT:3s}
read-timeout: ${STUDIO_METRICS_PROMETHEUS_READ_TIMEOUT:10s}
username: ${STUDIO_METRICS_PROMETHEUS_USERNAME:}
password: ${STUDIO_METRICS_PROMETHEUS_PASSWORD:}
bearer-token: ${STUDIO_METRICS_PROMETHEUS_BEARER_TOKEN:}
```

- `base-url` 是 Prometheus 或 Prometheus-compatible 服务的 URL 前缀;服务会在其后追加 `/api/v1/query_range`。
- 未配置 `base-url` 时,查询接口返回 HTTP 503。
- `connect-timeout` 默认 3 秒,`read-timeout` 默认 10 秒。
- Bearer Token 的优先级高于 Basic Auth;Basic Auth 必须同时配置 `username` 和 `password`。
- 密码和 Token 等敏感配置应通过环境变量或其他外部化配置传入,不应提交到代码仓库。

**错误响应:**

| HTTP 状态 | 场景 |
|-----------|------|
| `400` | JSON 无法解析、字段类型错误、请求校验失败或 PromQL 参数错误 |
| `422` | Prometheus 无法执行 PromQL 表达式 |
| `502` | 无法连接 Prometheus,或 Prometheus 返回非法响应 |
| `503` | Prometheus 未配置或暂时不可用 |
| `504` | Prometheus 查询超时 |

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,49 @@
*/
package com.rocketmq.studio.cluster.metrics;

import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Map;
import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MetricDataVO {
private String metric;
private List<long[]> values;
private String resultType;
private List<MetricSeriesVO> series;
private List<String> warnings;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class MetricSeriesVO {
private Map<String, String> labels;
private List<MetricSampleVO> values;
private List<MetricHistogramSampleVO> histograms;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class MetricSampleVO {
private double timestamp;
private String value;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class MetricHistogramSampleVO {
private double timestamp;
private JsonNode histogram;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
*/
package com.rocketmq.studio.cluster.metrics;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
Expand All @@ -25,9 +29,28 @@
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Prometheus range query")
public class MetricQueryDTO {
@Schema(description = "PromQL expression evaluated by Prometheus",
example = "sum(rate(rocketmq_messages_in_total[1m])) by (node_id)", minLength = 1,
requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "Metric query is required")
@Size(max = 4096, message = "Metric query must not exceed 4096 characters")
private String metric;

@Schema(description = "Range start as a Unix timestamp in seconds", example = "1784112606",
requiredMode = Schema.RequiredMode.REQUIRED)
@Positive(message = "Metric query start must be positive")
private long start;

@Schema(description = "Range end as a Unix timestamp in seconds", example = "1784114406",
requiredMode = Schema.RequiredMode.REQUIRED)
@Positive(message = "Metric query end must be positive")
private long end;

@Schema(description = "Prometheus query resolution step as a duration or number of seconds", example = "30s",
minLength = 1, requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "Metric query step is required")
@Size(max = 32, message = "Metric query step must not exceed 32 characters")
private String step;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
package com.rocketmq.studio.cluster.metrics;

import com.rocketmq.studio.common.domain.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -30,8 +34,19 @@ public class MetricsController {

private final MetricsService metricsService;

@Operation(summary = "Query Prometheus range metrics",
description = "Executes a PromQL range query against the configured Prometheus server")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Range query completed successfully",
useReturnTypeSchema = true),
@ApiResponse(responseCode = "400", description = "Invalid request or PromQL expression"),
@ApiResponse(responseCode = "422", description = "Prometheus could not execute the expression"),
@ApiResponse(responseCode = "502", description = "Prometheus connection or response failure"),
@ApiResponse(responseCode = "503", description = "Prometheus is unavailable or not configured"),
@ApiResponse(responseCode = "504", description = "Prometheus query timed out")
})
@PostMapping("/query")
public Result<MetricDataVO> query(@RequestBody MetricQueryDTO query) {
public Result<MetricDataVO> query(@Valid @RequestBody MetricQueryDTO query) {
return Result.ok(metricsService.query(query));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public class MetricsService {
private final MetricsSource metricsSource;

public MetricDataVO query(MetricQueryDTO query) {
log.info("Querying metrics: metric={}, start={}, end={}, step={}",
query.getMetric(), query.getStart(), query.getEnd(), query.getStep());
log.debug("Querying metrics: start={}, end={}, step={}",
query.getStart(), query.getEnd(), query.getStep());
return metricsSource.query(query);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.rocketmq.studio.cluster.metrics;

import lombok.Getter;

@Getter
public class PrometheusException extends RuntimeException {
private final int statusCode;

public PrometheusException(int statusCode, String message) {
super(message);
this.statusCode = statusCode;
}

public PrometheusException(int statusCode, String message, Throwable cause) {
super(message, cause);
this.statusCode = statusCode;
}
}
Loading