From d4ad336c5bdac9c1d3895189927d5973afde4d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=BCger?= Date: Mon, 30 Oct 2023 20:39:02 +0100 Subject: [PATCH] WIP: Initial support for CEL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds initial support for queries using the common expression language (CEL). ValueToJSON function is adapted from https://github.com/google/cel-go/blob/cfbf821f1b458533051306305a39b743db7c4bdb/codelab/codelab.go#L274 (Apache-2.0 Licensed) Signed-off-by: Manuel RĂ¼ger --- README.md | 20 +++++++- config/config.go | 13 ++++- examples/config.yml | 63 ++++++++++++++++++++--- exporter/collector.go | 115 ++++++++++++++++++++++++++++++++++++++---- exporter/util.go | 4 +- go.mod | 9 +++- go.sum | 15 ++++++ 7 files changed, 217 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f514a85d..d8d9ae3a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ json_exporter ======================== [![CircleCI](https://circleci.com/gh/prometheus-community/json_exporter.svg?style=svg)](https://circleci.com/gh/prometheus-community/json_exporter) -A [prometheus](https://prometheus.io/) exporter which scrapes remote JSON by JSONPath. +A [prometheus](https://prometheus.io/) exporter which scrapes remote JSON by JSONPath or [CEL (Common Expression Language)](https://github.com/google/cel-spec). - [Supported JSONPath Syntax](https://kubernetes.io/docs/reference/kubectl/jsonpath/) - [Examples configurations](/examples) @@ -21,6 +21,24 @@ Serving HTTP on :: port 8000 (http://[::]:8000/) ... ## TEST with 'default' module $ curl "http://localhost:7979/probe?module=default&target=http://localhost:8000/examples/data.json" +# HELP example_cel_global_value Example of a top-level global value scrape in the json using cel +# TYPE example_cel_global_value gauge +example_cel_global_value{environment="beta",location="planet-mars"} 1234 +# HELP example_cel_timestamped_value_count Example of a timestamped value scrape in the json +# TYPE example_cel_timestamped_value_count untyped +example_cel_timestamped_value_count{environment="beta"} 2 +# HELP example_cel_value_active Example of sub-level value scrapes from a json +# TYPE example_cel_value_active untyped +example_cel_value_active{environment="beta",id="id-A"} 1 +example_cel_value_active{environment="beta",id="id-C"} 1 +# HELP example_cel_value_boolean Example of sub-level value scrapes from a json +# TYPE example_cel_value_boolean untyped +example_cel_value_boolean{environment="beta",id="id-A"} 1 +example_cel_value_boolean{environment="beta",id="id-C"} 0 +# HELP example_cel_value_count Example of sub-level value scrapes from a json +# TYPE example_cel_value_count untyped +example_cel_value_count{environment="beta",id="id-A"} 1 +example_cel_value_count{environment="beta",id="id-C"} 3 # HELP example_global_value Example of a top-level global value scrape in the json # TYPE example_global_value untyped example_global_value{environment="beta",location="planet-mars"} 1234 diff --git a/config/config.go b/config/config.go index 6cd0accb..6a69ed58 100644 --- a/config/config.go +++ b/config/config.go @@ -23,6 +23,7 @@ import ( // Metric contains values that define a metric type Metric struct { Name string + Engine EngineType Path string Labels map[string]string Type ScrapeType @@ -44,7 +45,14 @@ type ValueType string const ( ValueTypeGauge ValueType = "gauge" ValueTypeCounter ValueType = "counter" - ValueTypeUntyped ValueType = "untyped" + ValueTypeUntyped ValueType = "untyped" // default +) + +type EngineType string + +const ( + EngineTypeJSONPath EngineType = "jsonpath" // default + EngineTypeCEL EngineType = "cel" ) // Config contains multiple modules. @@ -89,6 +97,9 @@ func LoadConfig(configPath string) (Config, error) { if module.Metrics[i].ValueType == "" { module.Metrics[i].ValueType = ValueTypeUntyped } + if module.Metrics[i].Engine == "" { + module.Metrics[i].Engine = EngineTypeJSONPath + } } } diff --git a/examples/config.yml b/examples/config.yml index 9d0745c0..bdd9ce94 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -9,7 +9,17 @@ modules: help: Example of a top-level global value scrape in the json labels: environment: beta # static label - location: 'planet-{.location}' # dynamic label + location: 'planet-{ .location }' # dynamic label + + - name: example_cel_global_value + engine: cel + path: '.counter' + help: Example of a top-level global value scrape in the json using cel + valuetype: 'gauge' + labels: + environment: "\"beta\"" # static label. Quotes need to be escaped for CEL + location: "\"planet-\"+.location" # dynamic label. Quotes need to be escaped for CEL + - name: example_timestamped_value type: object path: '{ .values[?(@.state == "INACTIVE")] }' @@ -18,18 +28,44 @@ modules: labels: environment: beta # static label values: - count: '{.count}' # dynamic value + count: '{ .count }' # dynamic value + + - name: example_cel_timestamped_value + type: object + engine: cel + path: ".values.filter(i, i.state == \"INACTIVE\")" + epochTimestamp: '.timestamp' + help: Example of a timestamped value scrape in the json + labels: + environment: "\"beta\"" # static label + values: + count: '.count' # dynamic value + - name: example_value type: object help: Example of sub-level value scrapes from a json - path: '{.values[?(@.state == "ACTIVE")]}' + path: '{ .values[?(@.state == "ACTIVE")] }' labels: environment: beta # static label - id: '{.id}' # dynamic label + id: '{ .id }' # dynamic label values: active: 1 # static value - count: '{.count}' # dynamic value - boolean: '{.some_boolean}' + count: '{ .count }' # dynamic value + boolean: '{ .some_boolean }' + + - name: example_cel_value + type: object + engine: cel + help: Example of sub-level value scrapes from a json + path: ".values.filter(i, i.state == \"ACTIVE\")" + labels: + environment: "\"beta\"" # static label + id: '.id' # dynamic label + values: + active: 1 # static value + count: '.count' # dynamic value + boolean: '.some_boolean' + animals: metrics: @@ -43,6 +79,17 @@ modules: values: population: '{ .population }' + - name: animal_cel + type: object + engine: cel + help: Example of top-level lists in a separate module + path: '[*]' + labels: + name: '.noun' + predator: '.predator' + values: + population: '.population' + ## HTTP connection configurations can be set in 'modules..http_client_config' field. For full http client config parameters, ref: https://pkg.go.dev/github.com/prometheus/common/config?tab=doc#HTTPClientConfig # # http_client_config: @@ -59,11 +106,11 @@ modules: ## If 'modueles..body' field is set, it will be sent by the exporter as the body content in the scrape request. The HTTP method will also be set as 'POST' in this case. # body: # content: | - # {"time_diff": "1m25s", "anotherVar": "some value"} + # { "time_diff": "1m25s", "anotherVar": "some value" } ## The body content can also be a Go Template (https://golang.org/pkg/text/template), with all the functions from the Sprig library (https://masterminds.github.io/sprig/) available. All the query parameters sent by prometheus in the scrape query to the exporter, are available in the template. # body: # content: | - # {"time_diff": "{{ duration `95` }}","anotherVar": "{{ .myVal | first }}"} + # { "time_diff": "{{ duration `95` }}","anotherVar": "{{ .myVal | first }}" } # templatize: true diff --git a/exporter/collector.go b/exporter/collector.go index 0a913897..cc34d301 100644 --- a/exporter/collector.go +++ b/exporter/collector.go @@ -16,11 +16,18 @@ package exporter import ( "bytes" "encoding/json" + "fmt" "log/slog" + "reflect" "time" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types/ref" "github.com/prometheus-community/json_exporter/config" "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + structpb "google.golang.org/protobuf/types/known/structpb" "k8s.io/client-go/util/jsonpath" ) @@ -33,6 +40,7 @@ type JSONMetricCollector struct { type JSONMetric struct { Desc *prometheus.Desc Type config.ScrapeType + EngineType config.EngineType KeyJSONPath string ValueJSONPath string LabelsJSONPaths []string @@ -50,7 +58,8 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { for _, m := range mc.JSONMetrics { switch m.Type { case config.ValueScrape: - value, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, false) + mc.Logger.Debug("Extracting value for metric", "path", m.KeyJSONPath, "metric", m.Desc) + value, err := extractValue(mc.Logger, m.EngineType, mc.Data, m.KeyJSONPath, false) if err != nil { mc.Logger.Error("Failed to extract value for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc) continue @@ -61,7 +70,7 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { m.Desc, m.ValueType, floatValue, - extractLabels(mc.Logger, mc.Data, m.LabelsJSONPaths)..., + extractLabels(mc.Logger, m.EngineType, mc.Data, m.LabelsJSONPaths)..., ) ch <- timestampMetric(mc.Logger, m, mc.Data, metric) } else { @@ -70,7 +79,8 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { } case config.ObjectScrape: - values, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, true) + mc.Logger.Debug("Extracting object for metric", "path", m.KeyJSONPath, "metric", m.Desc) + values, err := extractValue(mc.Logger, m.EngineType, mc.Data, m.KeyJSONPath, true) if err != nil { mc.Logger.Error("Failed to extract json objects for metric", "err", err, "metric", m.Desc) continue @@ -84,7 +94,7 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { mc.Logger.Error("Failed to marshal data to json", "path", m.ValueJSONPath, "err", err, "metric", m.Desc, "data", data) continue } - value, err := extractValue(mc.Logger, jdata, m.ValueJSONPath, false) + value, err := extractValue(mc.Logger, m.EngineType, jdata, m.ValueJSONPath, false) if err != nil { mc.Logger.Error("Failed to extract value for metric", "path", m.ValueJSONPath, "err", err, "metric", m.Desc) continue @@ -95,7 +105,7 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { m.Desc, m.ValueType, floatValue, - extractLabels(mc.Logger, jdata, m.LabelsJSONPaths)..., + extractLabels(mc.Logger, m.EngineType, jdata, m.LabelsJSONPaths)..., ) ch <- timestampMetric(mc.Logger, m, jdata, metric) } else { @@ -104,7 +114,7 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { } } } else { - mc.Logger.Error("Failed to convert extracted objects to json", "err", err, "metric", m.Desc) + mc.Logger.Error("Failed to convert extracted objects to json", "value", values, "err", err, "metric", m.Desc) continue } default: @@ -114,8 +124,19 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { } } +func extractValue(logger *slog.Logger, engine config.EngineType, data []byte, path string, enableJSONOutput bool) (string, error) { + switch engine { + case config.EngineTypeJSONPath: + return extractValueJSONPath(logger, data, path, enableJSONOutput) + case config.EngineTypeCEL: + return extractValueCEL(logger, data, path, enableJSONOutput) + default: + return "", fmt.Errorf("Unknown engine type: %s", engine) + } +} + // Returns the last matching value at the given json path -func extractValue(logger *slog.Logger, data []byte, path string, enableJSONOutput bool) (string, error) { +func extractValueJSONPath(logger *slog.Logger, data []byte, path string, enableJSONOutput bool) (string, error) { var jsonData interface{} buf := new(bytes.Buffer) @@ -147,11 +168,70 @@ func extractValue(logger *slog.Logger, data []byte, path string, enableJSONOutpu return buf.String(), nil } +// Returns the last matching value at the given json path +func extractValueCEL(logger *slog.Logger, data []byte, expression string, enableJSONOutput bool) (string, error) { + + var jsonData map[string]any + + err := json.Unmarshal(data, &jsonData) + if err != nil { + logger.Error("Failed to unmarshal data to json", "err", err, "data", data) + return "", err + } + + inputVars := make([]cel.EnvOption, 0, len(jsonData)) + for k := range jsonData { + inputVars = append(inputVars, cel.Variable(k, cel.DynType)) + } + + env, err := cel.NewEnv(inputVars...) + + if err != nil { + logger.Error("Failed to set up CEL environment", "err", err, "data", data) + return "", err + } + + ast, issues := env.Compile(expression) + if issues != nil && issues.Err() != nil { + logger.Error("CEL type-check error", "err", issues.String(), "expression", expression) + return "", err + } + prg, err := env.Program(ast) + if err != nil { + logger.Error("CEL program construction error", "err", err) + return "", err + } + + out, _, err := prg.Eval(jsonData) + if err != nil { + logger.Error("Failed to evaluate cel query", "err", err, "expression", expression, "data", jsonData) + return "", err + } + + // Since we are finally going to extract only float64, unquote if necessary + + //res, err := jsonpath.UnquoteExtend(fmt.Sprintf("%g", out)) + //if err == nil { + // level.Error(logger).Log("msg","Triggered") + // return res, nil + //} + logger.Error("Triggered later", "val", out) + if enableJSONOutput { + res, err := valueToJSON(out) + if err != nil { + return "", err + } + return res, nil + } + + return fmt.Sprintf("%v", out), nil +} + // Returns the list of labels created from the list of provided json paths -func extractLabels(logger *slog.Logger, data []byte, paths []string) []string { +func extractLabels(logger *slog.Logger, engine config.EngineType, data []byte, paths []string) []string { labels := make([]string, len(paths)) for i, path := range paths { - if result, err := extractValue(logger, data, path, false); err == nil { + if result, err := extractValue(logger, engine, data, path, false); err == nil { labels[i] = result } else { logger.Error("Failed to extract label value", "err", err, "path", path, "data", data) @@ -164,7 +244,7 @@ func timestampMetric(logger *slog.Logger, m JSONMetric, data []byte, pm promethe if m.EpochTimestampJSONPath == "" { return pm } - ts, err := extractValue(logger, data, m.EpochTimestampJSONPath, false) + ts, err := extractValue(logger, m.EngineType, data, m.EpochTimestampJSONPath, false) if err != nil { logger.Error("Failed to extract timestamp for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc) return pm @@ -177,3 +257,18 @@ func timestampMetric(logger *slog.Logger, m JSONMetric, data []byte, pm promethe timestamp := time.UnixMilli(epochTime) return prometheus.NewMetricWithTimestamp(timestamp, pm) } + +// valueToJSON converts the CEL type to a protobuf JSON representation and +// marshals the result to a string. +func valueToJSON(val ref.Val) (string, error) { + v, err := val.ConvertToNative(reflect.TypeOf(&structpb.Value{})) + if err != nil { + return "", err + } + marshaller := protojson.MarshalOptions{Indent: " "} + bytes, err := marshaller.Marshal(v.(proto.Message)) + if err != nil { + return "", err + } + return string(bytes), err +} diff --git a/exporter/util.go b/exporter/util.go index 957ef53c..950620f5 100644 --- a/exporter/util.go +++ b/exporter/util.go @@ -102,6 +102,7 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) { variableLabels, nil, ), + EngineType: metric.Engine, KeyJSONPath: metric.Path, LabelsJSONPaths: variableLabelsValues, ValueType: valueType, @@ -124,6 +125,7 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) { variableLabels, nil, ), + EngineType: metric.Engine, KeyJSONPath: metric.Path, ValueJSONPath: valuePath, LabelsJSONPaths: variableLabelsValues, @@ -133,7 +135,7 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) { metrics = append(metrics, jsonMetric) } default: - return nil, fmt.Errorf("Unknown metric type: '%s', for metric: '%s'", metric.Type, metric.Name) + return nil, fmt.Errorf("unknown metric type: '%s', for metric: '%s'", metric.Type, metric.Name) } } return metrics, nil diff --git a/go.mod b/go.mod index 81e825b0..25835d8d 100644 --- a/go.mod +++ b/go.mod @@ -5,18 +5,22 @@ go 1.22 require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/google/cel-go v0.22.0 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/common v0.60.0 github.com/prometheus/exporter-toolkit v0.13.0 + google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v2 v2.4.0 k8s.io/client-go v0.28.3 ) require ( + cel.dev/expr v0.18.0 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect @@ -34,12 +38,15 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect ) diff --git a/go.sum b/go.sum index fe39d46c..5cb4aaa3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= +cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -10,6 +12,8 @@ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjH github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -22,6 +26,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= +github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -68,14 +74,19 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= @@ -86,6 +97,10 @@ golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=