mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* tempo-cli: support relative time strings (now, now-1h) for start/end args Add support for relative time strings across all CLI commands that accept start/end arguments, matching Grafana's time string format. example: now-1h, now, now-30m, etc. Affected commands: - query api search - query api search-tags - query api search-tag-values - query api metrics query-range - query search Help text previously said "ISO8601 format" or showed a ISO 8601 format but the code was parsing with `time.RFC3339` (2006-01-02T15:04:05Z07:00). ISO 8601 is a broad standard that allows many representations including timestamps without timezone offsets. RFC 3339 is a strict profile of ISO 8601 that always requires a timezone offset (Z or +HH:MM). Help text is update to say RFC3339 with an example. We now accept a uniform time format across all commands: either RFC3339 absolute timestamps or relative time strings. query search: previously accepted timestamps without timezone (2024-03-15T10:30:00) using the layout "2006-01-02T15:04:05". This is valid ISO 8601 but not valid RFC 3339. These timestamps are now rejected. Users must include a timezone offset (e.g. 2024-03-15T10:30:00Z) or use relative time (e.g. now-1h). * update docs * Add CHANGELOG.md * trim spaces before parsing and add tests
238 lines
5.4 KiB
Go
238 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"github.com/gogo/protobuf/jsonpb"
|
|
"github.com/grafana/dskit/user"
|
|
|
|
"github.com/grafana/tempo/pkg/api"
|
|
"github.com/grafana/tempo/pkg/tempopb"
|
|
)
|
|
|
|
type metricsQueryCmd struct {
|
|
HostPort string `arg:"" help:"tempo host and port. scheme and path will be provided based on query type. e.g. localhost:3200"`
|
|
TraceQL string `arg:"" optional:"" help:"TraceQL query string"`
|
|
Start string `arg:"" optional:"" help:"start time in RFC3339 (e.g. 2006-01-02T15:04:05Z07:00) or relative (e.g. now-1h) format"`
|
|
End string `arg:"" optional:"" help:"end time in RFC3339 (e.g. 2006-01-02T15:04:05Z07:00) or relative (e.g. now) format"`
|
|
|
|
OrgID string `help:"optional orgID"`
|
|
UseGRPC bool `help:"stream search results over GRPC"`
|
|
Instant bool `help:"perform an instant query instead of a range query"`
|
|
PathPrefix string `help:"string to prefix all http paths with"`
|
|
Secure bool `help:"use https or grpc with TLS"`
|
|
}
|
|
|
|
func (cmd *metricsQueryCmd) Run(_ *globalOptions) error {
|
|
startDate, err := parseTime(cmd.Start)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
start := startDate.UnixNano()
|
|
|
|
endDate, err := parseTime(cmd.End)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
end := endDate.UnixNano()
|
|
|
|
if cmd.Instant {
|
|
req := &tempopb.QueryInstantRequest{
|
|
Query: cmd.TraceQL,
|
|
Start: uint64(start),
|
|
End: uint64(end),
|
|
}
|
|
|
|
if cmd.UseGRPC {
|
|
return cmd.queryInstantGRPC(req)
|
|
}
|
|
|
|
return cmd.queryInstantHTTP(req)
|
|
}
|
|
|
|
req := &tempopb.QueryRangeRequest{
|
|
Query: cmd.TraceQL,
|
|
Start: uint64(start),
|
|
End: uint64(end),
|
|
}
|
|
|
|
if cmd.UseGRPC {
|
|
return cmd.queryRangeGRPC(req)
|
|
}
|
|
|
|
return cmd.queryRangeHTTP(req)
|
|
}
|
|
|
|
func (cmd *metricsQueryCmd) queryRangeGRPC(req *tempopb.QueryRangeRequest) error {
|
|
ctx := user.InjectOrgID(context.Background(), cmd.OrgID)
|
|
ctx, err := user.InjectIntoGRPCRequest(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
creds, err := grpcTransportCredentials(cmd.Secure)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clientConn, err := grpc.NewClient(cmd.HostPort, creds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client := tempopb.NewStreamingQuerierClient(clientConn)
|
|
|
|
resp, err := client.MetricsQueryRange(ctx, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
searchResp, err := resp.Recv()
|
|
if searchResp != nil {
|
|
err = printAsJSON(searchResp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if errors.Is(err, io.EOF) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// nolint: goconst // goconst wants us to make http:// a const
|
|
func (cmd *metricsQueryCmd) queryRangeHTTP(req *tempopb.QueryRangeRequest) error {
|
|
httpReq, err := http.NewRequest("GET", httpScheme(cmd.Secure)+"://"+path.Join(cmd.HostPort, cmd.PathPrefix, api.PathMetricsQueryRange), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
httpReq = api.BuildQueryRangeRequest(httpReq, req, "")
|
|
httpReq.Header = http.Header{}
|
|
err = user.InjectOrgIDIntoHTTPRequest(user.InjectOrgID(context.Background(), cmd.OrgID), httpReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
httpResp, err := http.DefaultClient.Do(httpReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer httpResp.Body.Close()
|
|
|
|
body, err := io.ReadAll(httpResp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if httpResp.StatusCode != http.StatusOK {
|
|
return errors.New("failed to query. body: " + string(body) + " status: " + httpResp.Status)
|
|
}
|
|
|
|
resp := &tempopb.QueryRangeResponse{}
|
|
err = jsonpb.Unmarshal(bytes.NewReader(body), resp)
|
|
if err != nil {
|
|
panic("failed to parse resp: " + err.Error())
|
|
}
|
|
err = printAsJSON(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cmd *metricsQueryCmd) queryInstantGRPC(req *tempopb.QueryInstantRequest) error {
|
|
ctx := user.InjectOrgID(context.Background(), cmd.OrgID)
|
|
ctx, err := user.InjectIntoGRPCRequest(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
creds, err := grpcTransportCredentials(cmd.Secure)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clientConn, err := grpc.NewClient(cmd.HostPort, creds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client := tempopb.NewStreamingQuerierClient(clientConn)
|
|
|
|
resp, err := client.MetricsQueryInstant(ctx, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
searchResp, err := resp.Recv()
|
|
if searchResp != nil {
|
|
err = printAsJSON(searchResp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if errors.Is(err, io.EOF) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// nolint: goconst // goconst wants us to make http:// a const
|
|
func (cmd *metricsQueryCmd) queryInstantHTTP(req *tempopb.QueryInstantRequest) error {
|
|
httpReq, err := http.NewRequest("GET", httpScheme(cmd.Secure)+"://"+path.Join(cmd.HostPort, cmd.PathPrefix, api.PathMetricsQueryInstant), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
httpReq = api.BuildQueryInstantRequest(httpReq, req)
|
|
httpReq.Header = http.Header{}
|
|
err = user.InjectOrgIDIntoHTTPRequest(user.InjectOrgID(context.Background(), cmd.OrgID), httpReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
httpResp, err := http.DefaultClient.Do(httpReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer httpResp.Body.Close()
|
|
|
|
body, err := io.ReadAll(httpResp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if httpResp.StatusCode != http.StatusOK {
|
|
return errors.New("failed to query. body: " + string(body) + " status: " + httpResp.Status)
|
|
}
|
|
|
|
resp := &tempopb.QueryInstantResponse{}
|
|
err = jsonpb.Unmarshal(bytes.NewReader(body), resp)
|
|
if err != nil {
|
|
panic("failed to parse resp: " + err.Error())
|
|
}
|
|
err = printAsJSON(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|