/* * Datadog API for Go * * Please see the included LICENSE file for licensing information. * * Copyright 2017 by authors and contributors. */ package datadog import ( "encoding/json" "fmt" "net/url" "regexp" "strings" "time" ) // Define the available machine-readable SLO types const ( ServiceLevelObjectiveTypeMonitorID int = 0 ServiceLevelObjectiveTypeMetricID int = 1 ) // Define the available human-readable SLO types var ( ServiceLevelObjectiveTypeMonitor = "monitor" ServiceLevelObjectiveTypeMetric = "metric" ) // ServiceLevelObjectiveTypeFromID maps machine-readable type to human-readable type var ServiceLevelObjectiveTypeFromID = map[int]string{ ServiceLevelObjectiveTypeMonitorID: ServiceLevelObjectiveTypeMonitor, ServiceLevelObjectiveTypeMetricID: ServiceLevelObjectiveTypeMetric, } // ServiceLevelObjectiveTypeToID maps human-readable type to machine-readable type var ServiceLevelObjectiveTypeToID = map[string]int{ ServiceLevelObjectiveTypeMonitor: ServiceLevelObjectiveTypeMonitorID, ServiceLevelObjectiveTypeMetric: ServiceLevelObjectiveTypeMetricID, } // ServiceLevelObjectiveThreshold defines an SLO threshold and timeframe // For example it's the ` of within type ServiceLevelObjectiveThreshold struct { TimeFrame *string `json:"timeframe,omitempty"` Target *float64 `json:"target,omitempty"` TargetDisplay *string `json:"target_display,omitempty"` // Read-Only for monitor type Warning *float64 `json:"warning,omitempty"` WarningDisplay *string `json:"warning_display,omitempty"` // Read-Only for monitor type } const thresholdTolerance float64 = 1e-8 // Equal check if one threshold is equal to another. func (s *ServiceLevelObjectiveThreshold) Equal(o interface{}) bool { other, ok := o.(*ServiceLevelObjectiveThreshold) if !ok { return false } return s.GetTimeFrame() == other.GetTimeFrame() && Float64AlmostEqual(s.GetTarget(), other.GetTarget(), thresholdTolerance) && Float64AlmostEqual(s.GetWarning(), other.GetWarning(), thresholdTolerance) } // String implements Stringer func (s ServiceLevelObjectiveThreshold) String() string { return fmt.Sprintf("Threshold{timeframe=%s target=%f target_display=%s warning=%f warning_display=%s", s.GetTimeFrame(), s.GetTarget(), s.GetTargetDisplay(), s.GetWarning(), s.GetWarningDisplay()) } // ServiceLevelObjectiveMetricQuery represents a metric-based SLO definition query // Numerator is the sum of the `good` events // Denominator is the sum of the `total` events type ServiceLevelObjectiveMetricQuery struct { Numerator *string `json:"numerator,omitempty"` Denominator *string `json:"denominator,omitempty"` } // ServiceLevelObjectiveThresholds is a sortable array of ServiceLevelObjectiveThreshold(s) type ServiceLevelObjectiveThresholds []*ServiceLevelObjectiveThreshold // Len implements sort.Interface length func (s ServiceLevelObjectiveThresholds) Len() int { return len(s) } // Less implements sort.Interface less comparator func (s ServiceLevelObjectiveThresholds) Less(i, j int) bool { iDur, _ := ServiceLevelObjectiveTimeFrameToDuration(s[i].GetTimeFrame()) jDur, _ := ServiceLevelObjectiveTimeFrameToDuration(s[j].GetTimeFrame()) return iDur < jDur } // Swap implements sort.Interface swap method func (s ServiceLevelObjectiveThresholds) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // Equal check if one set of thresholds is equal to another. func (s ServiceLevelObjectiveThresholds) Equal(o interface{}) bool { other, ok := o.(ServiceLevelObjectiveThresholds) if !ok { return false } if len(s) != len(other) { // easy case return false } // compare one set from another sSet := make(map[string]*ServiceLevelObjectiveThreshold, 0) for _, t := range s { sSet[t.GetTimeFrame()] = t } oSet := make(map[string]*ServiceLevelObjectiveThreshold, 0) for _, t := range other { oSet[t.GetTimeFrame()] = t } for timeframe, t := range oSet { threshold, ok := sSet[timeframe] if !ok { // other contains more return false } if !threshold.Equal(t) { // they differ return false } // drop from sSet for efficiency delete(sSet, timeframe) } // if there are any remaining then they differ if len(sSet) > 0 { return false } return true } // ServiceLevelObjective defines the Service Level Objective entity type ServiceLevelObjective struct { // Common ID *string `json:"id,omitempty"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Tags []string `json:"tags,omitempty"` Thresholds ServiceLevelObjectiveThresholds `json:"thresholds,omitempty"` Type *string `json:"type,omitempty"` TypeID *int `json:"type_id,omitempty"` // Read-Only // SLI definition Query *ServiceLevelObjectiveMetricQuery `json:"query,omitempty"` MonitorIDs []int `json:"monitor_ids,omitempty"` MonitorSearch *string `json:"monitor_search,omitempty"` Groups []string `json:"groups,omitempty"` // Informational MonitorTags []string `json:"monitor_tags,omitempty"` // Read-Only Creator *Creator `json:"creator,omitempty"` // Read-Only CreatedAt *int `json:"created_at,omitempty"` // Read-Only ModifiedAt *int `json:"modified_at,omitempty"` // Read-Only } // MarshalJSON implements custom marshaler to ignore some fields func (s *ServiceLevelObjective) MarshalJSON() ([]byte, error) { var output struct { ID *string `json:"id,omitempty"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Tags []string `json:"tags,omitempty"` Thresholds ServiceLevelObjectiveThresholds `json:"thresholds,omitempty"` Type *string `json:"type,omitempty"` // SLI definition Query *ServiceLevelObjectiveMetricQuery `json:"query,omitempty"` MonitorIDs []int `json:"monitor_ids,omitempty"` MonitorSearch *string `json:"monitor_search,omitempty"` Groups []string `json:"groups,omitempty"` } output.ID = s.ID output.Name = s.Name output.Description = s.Description output.Tags = s.Tags output.Thresholds = s.Thresholds output.Type = s.Type output.Query = s.Query output.MonitorIDs = s.MonitorIDs output.MonitorSearch = s.MonitorSearch output.Groups = s.Groups return json.Marshal(&output) } var sloTimeFrameToDurationRegex = regexp.MustCompile(`(?P\d+)(?P(d))`) // ServiceLevelObjectiveTimeFrameToDuration will convert a timeframe into a duration func ServiceLevelObjectiveTimeFrameToDuration(timeframe string) (time.Duration, error) { match := sloTimeFrameToDurationRegex.FindStringSubmatch(timeframe) result := make(map[string]string) for i, name := range sloTimeFrameToDurationRegex.SubexpNames() { if i != 0 && name != "" { result[name] = match[i] } } if len(result) != 2 { return 0, fmt.Errorf("invalid timeframe specified: '%s'", timeframe) } qty, err := json.Number(result["quantity"]).Int64() if err != nil { return 0, fmt.Errorf("invalid timeframe specified, could not convert quantity to number") } if qty <= 0 { return 0, fmt.Errorf("invalid timeframe specified, quantity must be a positive number") } switch result["unit"] { // FUTURE: will support more time frames, hence the switch here. default: // only matches on `d` currently, so this is simple return time.Hour * 24 * time.Duration(qty), nil } } // CreateServiceLevelObjective adds a new service level objective to the system. This returns a pointer // to the service level objective so you can pass that to UpdateServiceLevelObjective or DeleteServiceLevelObjective // later if needed. func (client *Client) CreateServiceLevelObjective(slo *ServiceLevelObjective) (*ServiceLevelObjective, error) { var out reqServiceLevelObjectives if slo == nil { return nil, fmt.Errorf("no SLO specified") } if err := client.doJsonRequest("POST", "/v1/slo", slo, &out); err != nil { return nil, err } if out.Error != "" { return nil, fmt.Errorf(out.Error) } return out.Data[0], nil } // UpdateServiceLevelObjective takes a service level objective that was previously retrieved through some method // and sends it back to the server. func (client *Client) UpdateServiceLevelObjective(slo *ServiceLevelObjective) (*ServiceLevelObjective, error) { var out reqServiceLevelObjectives if slo == nil { return nil, fmt.Errorf("no SLO specified") } if _, ok := slo.GetIDOk(); !ok { return nil, fmt.Errorf("SLO must be created first") } if err := client.doJsonRequest("PUT", fmt.Sprintf("/v1/slo/%s", slo.GetID()), slo, &out); err != nil { return nil, err } if out.Error != "" { return nil, fmt.Errorf(out.Error) } return out.Data[0], nil } type reqServiceLevelObjectives struct { Data []*ServiceLevelObjective `json:"data"` Error string `json:"error"` } // SearchServiceLevelObjectives searches for service level objectives by search criteria. // limit will limit the amount of SLO's returned, the API will enforce a maximum and default to a minimum if not specified func (client *Client) SearchServiceLevelObjectives(limit int, offset int, query string, ids []string) ([]*ServiceLevelObjective, error) { var out reqServiceLevelObjectives uriValues := make(url.Values, 0) if limit > 0 { uriValues.Set("limit", fmt.Sprintf("%d", limit)) } if offset >= 0 { uriValues.Set("offset", fmt.Sprintf("%d", offset)) } // Either use `query` or use `ids` hasQuery := strings.TrimSpace(query) != "" hasIDs := len(ids) > 0 if hasQuery && hasIDs { return nil, fmt.Errorf("invalid search: must specify either ids OR query, not both") } // specify by query if hasQuery { uriValues.Set("query", query) } // specify by `ids` if hasIDs { uriValues.Set("ids", strings.Join(ids, ",")) } uri := "/v1/slo?" + uriValues.Encode() if err := client.doJsonRequest("GET", uri, nil, &out); err != nil { return nil, err } if out.Error != "" { return nil, fmt.Errorf(out.Error) } return out.Data, nil } type reqSingleServiceLevelObjective struct { Data *ServiceLevelObjective `json:"data"` Error string `json:"error"` } // GetServiceLevelObjective retrieves an service level objective by identifier. func (client *Client) GetServiceLevelObjective(id string) (*ServiceLevelObjective, error) { var out reqSingleServiceLevelObjective if id == "" { return nil, fmt.Errorf("no SLO specified") } if err := client.doJsonRequest("GET", fmt.Sprintf("/v1/slo/%s", id), nil, &out); err != nil { return nil, err } if out.Error != "" { return nil, fmt.Errorf(out.Error) } return out.Data, nil } type reqDeleteResp struct { Data []string `json:"data"` Error string `json:"error"` } // DeleteServiceLevelObjective removes an service level objective from the system. func (client *Client) DeleteServiceLevelObjective(id string) error { var out reqDeleteResp if id == "" { return fmt.Errorf("no SLO specified") } if err := client.doJsonRequest("DELETE", fmt.Sprintf("/v1/slo/%s", id), nil, &out); err != nil { return err } if out.Error != "" { return fmt.Errorf(out.Error) } return nil } // DeleteServiceLevelObjectives removes multiple service level objective from the system by id. func (client *Client) DeleteServiceLevelObjectives(ids []string) error { var out reqDeleteResp if len(ids) == 0 { return fmt.Errorf("no SLOs specified") } if err := client.doJsonRequest("DELETE", "/v1/slo", ids, &out); err != nil { return err } if out.Error != "" { return fmt.Errorf(out.Error) } return nil } // ServiceLevelObjectiveDeleteTimeFramesResponse is the response unique to the delete individual time-frames request // this is read-only type ServiceLevelObjectiveDeleteTimeFramesResponse struct { DeletedIDs []string `json:"deleted"` UpdatedIDs []string `json:"updated"` } // ServiceLevelObjectiveDeleteTimeFramesError is the error specific to deleting individual time frames. // It contains more detailed information than the standard error. type ServiceLevelObjectiveDeleteTimeFramesError struct { ID *string `json:"id"` TimeFrame *string `json:"timeframe"` Message *string `json:"message"` } // Error computes the human readable error func (e ServiceLevelObjectiveDeleteTimeFramesError) Error() string { return fmt.Sprintf("error=%s id=%s for timeframe=%s", e.GetMessage(), e.GetID(), e.GetTimeFrame()) } type timeframesDeleteResp struct { Data *ServiceLevelObjectiveDeleteTimeFramesResponse `json:"data"` Errors []*ServiceLevelObjectiveDeleteTimeFramesError `json:"errors"` } // DeleteServiceLevelObjectiveTimeFrames will delete SLO timeframes individually. // This is useful if you have a SLO with 3 time windows and only need to delete some of the time windows. // It will do a full delete if all time windows are removed as a result. // // Example: // SLO `12345678901234567890123456789012` was defined with 2 time frames: "7d" and "30d" // SLO `abcdefabcdefabcdefabcdefabcdefab` was defined with 2 time frames: "30d" and "90d" // // When we delete `7d` from `12345678901234567890123456789012` we still have `30d` timeframe remaining, hence this is "updated" // When we delete `30d` and `90d` from `abcdefabcdefabcdefabcdefabcdefab` we are left with 0 time frames, hence this is "deleted" // and the entire SLO config is deleted func (client *Client) DeleteServiceLevelObjectiveTimeFrames(timeframeByID map[string][]string) (*ServiceLevelObjectiveDeleteTimeFramesResponse, error) { var out timeframesDeleteResp if len(timeframeByID) == 0 { return nil, fmt.Errorf("nothing specified") } if err := client.doJsonRequest("POST", "/v1/slo/bulk_delete", &timeframeByID, &out); err != nil { return nil, err } if out.Errors != nil && len(out.Errors) > 0 { errMsgs := make([]string, 0) for _, e := range out.Errors { errMsgs = append(errMsgs, e.Error()) } return nil, fmt.Errorf("errors deleting timeframes: %s", strings.Join(errMsgs, ",")) } return out.Data, nil } // ServiceLevelObjectivesCanDeleteResponse is the response for a check can delete SLO endpoint. type ServiceLevelObjectivesCanDeleteResponse struct { Data struct { OK []string `json:"ok"` } `json:"data"` Errors map[string]string `json:"errors"` } // CheckCanDeleteServiceLevelObjectives checks if the SLO is referenced within Datadog. // This is useful to prevent accidental deletion. func (client *Client) CheckCanDeleteServiceLevelObjectives(ids []string) (*ServiceLevelObjectivesCanDeleteResponse, error) { var out ServiceLevelObjectivesCanDeleteResponse if len(ids) == 0 { return nil, fmt.Errorf("nothing specified") } uriValues := make(url.Values, 0) uriValues.Set("ids", strings.Join(ids, ",")) uri := "/v1/slo/can_delete?" + uriValues.Encode() if err := client.doJsonRequest("GET", uri, nil, &out); err != nil { return nil, err } return &out, nil } // ServiceLevelObjectiveHistorySeriesPoint is a convenient wrapper for (timestamp, value) history data response. type ServiceLevelObjectiveHistorySeriesPoint [2]json.Number // ServiceLevelObjectiveHistoryMetricSeriesData contains the `batch_query` like history data for `metric` based SLOs type ServiceLevelObjectiveHistoryMetricSeriesData struct { Count int64 `json:"count"` Sum json.Number `json:"sum"` MetaData struct { QueryIndex int `json:"query_index"` Aggregator string `json:"aggr"` Scope string `json:"scope"` Metric string `json:"metric"` Expression string `json:"expression"` Unit *string `json:"unit"` } `json:"metadata"` Values []json.Number `json:"values"` Times []int64 `json:"times"` } // ValuesAsFloats will transform all the values into a slice of float64 func (d *ServiceLevelObjectiveHistoryMetricSeriesData) ValuesAsFloats() ([]float64, error) { out := make([]float64, len(d.Values)) for i := 0; i < len(d.Values); i++ { v, err := d.Values[i].Float64() if err != nil { return out, fmt.Errorf("could not deserialize value at index %d: %s", i, err) } out[i] = v } return out, nil } // ValuesAsInt64s will transform all the values into a slice of int64 func (d *ServiceLevelObjectiveHistoryMetricSeriesData) ValuesAsInt64s() ([]int64, error) { out := make([]int64, len(d.Values)) for i := 0; i < len(d.Values); i++ { v, err := d.Values[i].Int64() if err != nil { return out, fmt.Errorf("could not deserialize value at index %d: %s", i, err) } out[i] = v } return out, nil } // ServiceLevelObjectiveHistoryMetricSeries defines the SLO history data response for `metric` type SLOs type ServiceLevelObjectiveHistoryMetricSeries struct { ResultType string `json:"res_type"` Interval int `json:"interval"` ResponseVersion json.Number `json:"resp_version"` Query string `json:"query"` // a CSV of , queries Message string `json:"message"` // optional message if there are specific query issues/warnings Numerator *ServiceLevelObjectiveHistoryMetricSeriesData `json:"numerator"` Denominator *ServiceLevelObjectiveHistoryMetricSeriesData `json:"denominator"` } // ServiceLevelObjectiveHistoryMonitorSeries defines the SLO history data response for `monitor` type SLOs type ServiceLevelObjectiveHistoryMonitorSeries struct { Uptime float32 `json:"uptime"` SpanPrecision json.Number `json:"span_precision"` Name string `json:"name"` Precision map[string]json.Number `json:"precision"` Preview bool `json:"preview"` History []ServiceLevelObjectiveHistorySeriesPoint `json:"history"` } // ServiceLevelObjectiveHistoryOverall defines the overall SLO history data response // for `monitor` type SLOs there is an additional `History` property that rolls up the overall state correctly. type ServiceLevelObjectiveHistoryOverall struct { Uptime float32 `json:"uptime"` SpanPrecision json.Number `json:"span_precision"` Name string `json:"name"` Precision map[string]json.Number `json:"precision"` Preview bool `json:"preview"` // Monitor extension History []ServiceLevelObjectiveHistorySeriesPoint `json:"history"` } // ServiceLevelObjectiveHistoryResponseData contains the SLO history data response. // for `monitor` based SLOs use the `Groups` property for historical data along with the `Overall.History` // for `metric` based SLOs use the `Metrics` property for historical data. This contains `batch_query` like response // data type ServiceLevelObjectiveHistoryResponseData struct { Errors []string `json:"errors"` ToTs int64 `json:"to_ts"` FromTs int64 `json:"from_ts"` Thresholds map[string]ServiceLevelObjectiveThreshold `json:"thresholds"` Overall *ServiceLevelObjectiveHistoryOverall `json:"overall"` // metric based SLO Metrics *ServiceLevelObjectiveHistoryMetricSeries `json:"series"` // monitor based SLO Groups []*ServiceLevelObjectiveHistoryMonitorSeries `json:"groups"` } // ServiceLevelObjectiveHistoryResponse is the canonical response for SLO history data. type ServiceLevelObjectiveHistoryResponse struct { Data *ServiceLevelObjectiveHistoryResponseData `json:"data"` Error *string `json:"error"` } // GetServiceLevelObjectiveHistory will retrieve the history data for a given SLO and provided from/to times func (client *Client) GetServiceLevelObjectiveHistory(id string, fromTs time.Time, toTs time.Time) (*ServiceLevelObjectiveHistoryResponse, error) { var out ServiceLevelObjectiveHistoryResponse if id == "" { return nil, fmt.Errorf("nothing specified") } if !toTs.After(fromTs) { return nil, fmt.Errorf("toTs must be after fromTs") } uriValues := make(url.Values, 0) uriValues.Set("from_ts", fmt.Sprintf("%d", fromTs.Unix())) uriValues.Set("to_ts", fmt.Sprintf("%d", toTs.Unix())) uri := fmt.Sprintf("/v1/slo/%s/history?%s", id, uriValues.Encode()) if err := client.doJsonRequest("GET", uri, nil, &out); err != nil { return nil, err } return &out, nil }