Skip to content
Merged
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
2 changes: 1 addition & 1 deletion data_sources.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

169 changes: 169 additions & 0 deletions datasource_query_params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package flashduty

import (
"encoding/json"
"errors"
"fmt"
)

// Request-side params types for the `<type>.query` datasource tools invoked
// through DataSourcesService.ToolsInvoke.
//
// These schemas are hand-written (not generated): they are not reachable from
// any requestBody in the OpenAPI spec — the invoke request carries `params` as
// raw JSON — so the generator would emit them as response-side types, where
// epoch-millis fields marshal to RFC3339 strings and optional fields lose
// `,omitempty`. Here, optional fields are pointers: a nil pointer omits the
// key so the server keeps its default, while a non-nil pointer sends the value
// explicitly (explicit null is rejected by the server, so there is no way to
// send one). Use the Bool/Int64/String helpers in ptr.go to set them.

// DatasourceQueryExecution describes when a `<type>.query` tool evaluates.
//
// Kind selects the evaluation mode:
// - `instant` evaluates once at ToMS; only ToMS is set.
// - `range` evaluates a stepped series over [FromMS, ToMS]; MaxDataPoints is
// required and bounds the returned points (the server computes the step).
// - `window` evaluates one bounded window [FromMS, ToMS].
//
// The reserved `step_seconds` field is not modeled and must stay omitted.
type DatasourceQueryExecution struct {
Kind string `json:"kind"`
// Window or range start as a Unix epoch timestamp in milliseconds.
FromMS *int64 `json:"from_ms,omitempty"`
// Query end time as a Unix epoch timestamp in milliseconds; for `instant`
// it is the evaluation timestamp.
ToMS *int64 `json:"to_ms,omitempty"`
// `range` only, required: maximum returned data points.
MaxDataPoints *int64 `json:"max_data_points,omitempty"`
// `range` only, optional: positive lower bound in seconds for the computed
// step.
MinStepSeconds *int64 `json:"min_step_seconds,omitempty"`
}

// PrometheusQueryParams are the params for `prometheus.query`. Expr is PromQL.
// Execution.Kind must be `instant` or `range`; `instant` accepts only ToMS.
type PrometheusQueryParams struct {
Expr string `json:"expr"`
Execution DatasourceQueryExecution `json:"execution"`
}

// MySQLQueryParams are the params for `mysql.query`. Expr is a single
// read-only SQL statement. Execution.Kind must be `window`.
type MySQLQueryParams struct {
Expr string `json:"expr"`
Execution DatasourceQueryExecution `json:"execution"`
}

// PostgresQueryParams are the params for `postgres.query`. Expr is a single
// read-only SQL statement. Execution.Kind must be `window`.
type PostgresQueryParams struct {
Expr string `json:"expr"`
Execution DatasourceQueryExecution `json:"execution"`
}

// OracleQueryParams are the params for `oracle.query`. Expr is a single
// read-only SQL statement. Execution.Kind must be `window`.
type OracleQueryParams struct {
Expr string `json:"expr"`
Execution DatasourceQueryExecution `json:"execution"`
}

// ClickHouseQueryParams are the params for `clickhouse.query`. Expr is a
// single read-only SQL statement. Execution.Kind must be `window`.
type ClickHouseQueryParams struct {
Expr string `json:"expr"`
Execution DatasourceQueryExecution `json:"execution"`
}

// ElasticsearchQueryParams are the params for `elasticsearch.query`. Expr is
// a single SQL statement; Elasticsearch DSL queries are not supported.
// Execution.Kind must be `window`.
type ElasticsearchQueryParams struct {
Expr string `json:"expr"`
Execution DatasourceQueryExecution `json:"execution"`
}

// LokiQueryParams are the params for `loki.query`. Expr is LogQL.
// Execution.Kind must be `instant` or `range`; `instant` keeps the full time
// context so `$__auto` ranges resolve. Limit and Direction only apply to
// raw-log results.
type LokiQueryParams struct {
Expr string `json:"expr"`
Execution DatasourceQueryExecution `json:"execution"`
// Maximum raw-log entries to return (1–1000); only bounds raw logs, not
// SQL rows or scanned data.
Limit *int64 `json:"limit,omitempty"`
// Raw-log retrieval order: `latest` or `earliest`.
Direction *string `json:"direction,omitempty"`
}

// VictoriaLogsQueryParams are the params for `victorialogs.query`. Expr is
// LogsQL. Use a `window` execution for raw logs (Limit/Direction allowed) or
// an `instant`/`range` execution with FromMS for stats queries
// (Limit/Direction rejected).
type VictoriaLogsQueryParams struct {
Expr string `json:"expr"`
Execution DatasourceQueryExecution `json:"execution"`
// Maximum raw-log entries to return (1–1000); only bounds raw logs, not
// SQL rows or scanned data.
Limit *int64 `json:"limit,omitempty"`
// Raw-log retrieval order: `latest` or `earliest`.
Direction *string `json:"direction,omitempty"`
}

// SLSQueryParams are the params for `sls.query` on Alibaba Cloud SLS.
// Execution.Kind must be `window`.
type SLSQueryParams struct {
Expr string `json:"expr"`
Execution DatasourceQueryExecution `json:"execution"`
// SLS project name.
Project string `json:"project"`
// SLS logstore name.
Logstore string `json:"logstore"`
// Whether to run the query with SLS PowerSQL. A pointer because explicit
// false is a real value distinct from the executor default.
PowerSQL *bool `json:"powersql,omitempty"`
// Maximum raw-log entries to return (1–100); only bounds raw logs, not
// SQL rows or scanned data.
Limit *int64 `json:"limit,omitempty"`
// Raw-log retrieval order: `latest` or `earliest`.
Direction *string `json:"direction,omitempty"`
}

// TencentCLSQueryParams are the params for `tencent_cls.query` on Tencent
// Cloud CLS. Execution.Kind must be `window`.
type TencentCLSQueryParams struct {
Expr string `json:"expr"`
Execution DatasourceQueryExecution `json:"execution"`
// Tencent Cloud region, e.g. `ap-guangzhou`.
Region string `json:"region"`
// CLS log topic ID.
TopicID string `json:"topic_id"`
// Search syntax: `cql` or `lucene`.
Syntax string `json:"syntax"`
// Maximum raw-log entries to return (1–1000); only bounds raw logs, not
// SQL rows or scanned data.
Limit *int64 `json:"limit,omitempty"`
// Raw-log retrieval order: `latest` or `earliest`.
Direction *string `json:"direction,omitempty"`
}

// NewDatasourceQueryInvokeRequest builds a ToolsInvoke request for a
// `<type>.query` tool by marshaling params (one of the *QueryParams types
// above) into the request's raw JSON Params. A nil params returns an error:
// query tools must not omit their params.
func NewDatasourceQueryInvokeRequest(datasourceID uint64, tool string, params any) (*DatasourceToolInvokeRequest, error) {
if params == nil {
return nil, errors.New("flashduty: query tool params must not be nil")
}
raw, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("flashduty: marshal query tool params: %w", err)
}
return &DatasourceToolInvokeRequest{
DatasourceID: datasourceID,
Tool: tool,
Params: json.RawMessage(raw),
}, nil
}
99 changes: 99 additions & 0 deletions datasource_query_params_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package flashduty

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"testing"
)

func TestDatasourceQueryInvokeRequestSLSWireFormat(t *testing.T) {
// Beyond 2^53: must survive the wire as exact integers.
from := int64(9007199254740993)
to := int64(9007199254740994)
params := &SLSQueryParams{
Expr: "status: 500",
Project: "my-project",
Logstore: "my-logstore",
PowerSQL: Bool(false),
Limit: Int64(50),
Execution: DatasourceQueryExecution{
Kind: "window",
FromMS: Int64(from),
ToMS: Int64(to),
},
}
req, err := NewDatasourceQueryInvokeRequest(42, "sls.query", params)
if err != nil {
t.Fatal(err)
}
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/monit/datasource/tools/invoke" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
body, _ := io.ReadAll(r.Body)
if !strings.Contains(string(body), `"powersql":false`) {
t.Errorf("powersql lost or stringified: %s", body)
}
if !strings.Contains(string(body), `"from_ms":9007199254740993`) || !strings.Contains(string(body), `"to_ms":9007199254740994`) {
t.Errorf("epoch millis lost precision or changed form: %s", body)
}
decoder := json.NewDecoder(strings.NewReader(string(body)))
decoder.UseNumber()
var input map[string]any
if err := decoder.Decode(&input); err != nil {
t.Fatal(err)
}
execution, ok := input["params"].(map[string]any)["execution"].(map[string]any)
if !ok {
t.Fatalf("execution missing: %s", body)
}
fromWire, ok := execution["from_ms"].(json.Number)
if !ok {
t.Fatalf("from_ms is not a JSON number: %s", body)
}
if fromWire.String() != "9007199254740993" {
t.Errorf("from_ms precision lost: %s", fromWire)
}
w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprintf(w, `{"request_id":"trace-query","data":{"datasource_id":42,"tool":"sls.query","data":{"rows":[]}}}`)
})
result, _, err := client.DataSources.ToolsInvoke(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if result.Tool != "sls.query" || string(result.Data) != `{"rows":[]}` {
t.Fatalf("response changed: %+v", result)
}
}

func TestVictoriaLogsQueryParamsInstantStatsOmitsRawLogFields(t *testing.T) {
params := &VictoriaLogsQueryParams{
Expr: `* | stats count() as total`,
Execution: DatasourceQueryExecution{
Kind: "instant",
FromMS: Int64(1757433600000),
ToMS: Int64(1757520000000),
},
}
raw, err := json.Marshal(params)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), `"limit"`) || strings.Contains(string(raw), `"direction"`) {
t.Fatalf("instant stats params carry raw-log keys: %s", raw)
}
if !strings.Contains(string(raw), `"from_ms":1757433600000`) {
t.Fatalf("from_ms missing or not numeric: %s", raw)
}
}

func TestNewDatasourceQueryInvokeRequestRejectsNilParams(t *testing.T) {
req, err := NewDatasourceQueryInvokeRequest(42, "sls.query", nil)
if err == nil || req != nil {
t.Fatalf("nil params must fail: req=%+v err=%v", req, err)
}
}
18 changes: 18 additions & 0 deletions internal/cmd/gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ func run() error {
"DutyError": true,
"AutomationRuleUpdateRequest": true, // hand-written to preserve partial-update pointer semantics.
"SkillUploadRequest": true, // multipart form schema; the hand-written WriteUpload carries the file as an io.Reader.
// The <type>.query tool params are request-side schemas, but they are
// not reachable from any requestBody (the invoke request carries
// params as x-flashduty-raw-json), so the generator would emit them
// as response-side types: epoch-millis fields would become
// TimestampMilli (marshaling to RFC3339 strings) and optional fields
// would lose omitempty. They are hand-written in
// datasource_query_params.go with request-side pointer semantics.
"DatasourceQueryExecution": true,
"PrometheusQueryParams": true,
"MySQLQueryParams": true,
"PostgresQueryParams": true,
"OracleQueryParams": true,
"ClickHouseQueryParams": true,
"ElasticsearchQueryParams": true,
"LokiQueryParams": true,
"VictoriaLogsQueryParams": true,
"SLSQueryParams": true,
"TencentCLSQueryParams": true,
},
queued: map[string]bool{},
synth: map[string]any{},
Expand Down
4 changes: 2 additions & 2 deletions models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading