blob: 0b0981ce1038bd694319baf71fcd54225f9fc4f4 [file] [log] [blame]
package trafficstats
/*
* 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.
*/
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/lib/go-log"
"github.com/apache/trafficcontrol/lib/go-rfc"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
influx "github.com/influxdata/influxdb/client/v2"
)
const (
dsTenantIDFromXMLIDQuery = `
SELECT tenant_id
FROM deliveryservice
WHERE xml_id = $1`
xmlidFromIDQuery = `
SELECT xml_id
FROM deliveryservice
WHERE id = $1`
// TODO: Pretty sure all of this could actually be calculated using the fetched data (assuming an
// interval is given). Check to see if that's faster than doing another synchronous HTTP request.
dsSummaryQuery = `
SELECT mean(value) AS "average",
percentile(value, 5) AS "fifthPercentile",
percentile(value, 95) AS "ninetyFifthPercentile",
percentile(value, 98) AS "ninetyEighthPercentile",
min(value) AS "min",
max(value) AS "max",
count(value) AS "count"
FROM "%s"."monthly"."%s.ds.1min"
WHERE time >= $start
AND time <= $end
AND cachegroup = 'total'
AND deliveryservice = $xmlid`
dsSeriesQuery = `
SELECT mean(value)
FROM "%s"."monthly"."%s.ds.1min"
WHERE cachegroup = 'total'
AND deliveryservice = $xmlid
AND time >= $start
AND time <= $end
GROUP BY time(%s, %s), cachegroup%s`
)
func dsConfigFromRequest(r *http.Request, i *api.APIInfo) (tc.TrafficDSStatsConfig, int, error) {
c := tc.TrafficDSStatsConfig{}
statsConfig, rc, e := tsConfigFromRequest(r, i)
if e != nil {
return c, rc, e
}
c.TrafficStatsConfig = statsConfig
c.MetricType = i.Params["metricType"]
if _, found := findMetric(i.Config.ConfigTrafficOpsGolang.SupportedDSMetrics, c.MetricType); !found {
e = fmt.Errorf("Metric is not supported: %s", c.MetricType)
return c, http.StatusBadRequest, e
}
var ok bool
if c.DeliveryService, ok = i.Params["deliveryServiceName"]; !ok {
if c.DeliveryService, ok = i.Params["deliveryService"]; !ok {
e = errors.New("You must specify deliveryService or deliveryServiceName!")
return c, http.StatusBadRequest, e
}
if dsID, err := strconv.ParseUint(c.DeliveryService, 10, 64); err == nil {
// sql.ErrNoRows does not *necessarily* mean the DS doesn't exist - an XMLID can simply
// be numeric, and so it was wrong to treat it as an ID in the first place.
xmlid := c.DeliveryService
var exists bool
if exists, c.DeliveryService, err = getXMLIDFromID(dsID, i.Tx.Tx); err != nil {
log.Errorf("Converting DSID to XMLID: %v", err)
e = errors.New("Internal Server Error")
return c, http.StatusInternalServerError, e
} else if !exists {
c.DeliveryService = xmlid
}
}
}
return c, http.StatusOK, nil
}
// GetDSStats handler for getting deliveryservice stats
func GetDSStats(w http.ResponseWriter, r *http.Request) {
// Perl didn't require "interval", but it would only return summary data if it was not given
inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"metricType", "startDate", "endDate"}, nil)
tx := inf.Tx.Tx
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
defer inf.Close()
var c tc.TrafficDSStatsConfig
if c, errCode, userErr = dsConfigFromRequest(r, inf); userErr != nil {
sysErr = fmt.Errorf("Unable to process deliveryservice_stats request: %v", userErr)
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
client, err := inf.CreateInfluxClient()
if err != nil {
errCode = http.StatusInternalServerError
sysErr = err
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
} else if client == nil {
sysErr = errors.New("Traffic Stats is not configured, but DS stats were requested")
errCode = http.StatusInternalServerError
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
}
defer (*client).Close()
exists, dsTenant, err := dsTenantIDFromXMLID(c.DeliveryService, tx)
if err != nil {
sysErr = err
errCode = http.StatusInternalServerError
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
} else if !exists {
userErr = fmt.Errorf("No such Delivery Service: %s", c.DeliveryService)
errCode = http.StatusNotFound
api.HandleErr(w, r, tx, errCode, userErr, nil)
return
}
authorized, err := tenant.IsResourceAuthorizedToUserTx(int(dsTenant), inf.User, tx)
if err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
return
} else if !authorized {
// If the Tenant is not authorized to use the resource, then we DON'T tell them that.
// Instead, we don't disclose that such a Delivery Service exists at all - in keeping with
// the behavior of /deliveryservices
// This is different from what Perl used to do, but then again Perl didn't check tenancy at
// all.
userErr = fmt.Errorf("No such Delivery Service: %s", c.DeliveryService)
sysErr = fmt.Errorf("GetDSStats: unauthorized Tenant (#%d) access", inf.User.TenantID)
errCode = http.StatusNotFound
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
handleRequest(w, r, client, c, inf)
}
func handleRequest(w http.ResponseWriter, r *http.Request, client *influx.Client, cfg tc.TrafficDSStatsConfig, inf *api.APIInfo) {
// TODO: as above, this could be done on TO itself, thus sending only one synchronous request
// per hit on this endpoint, rather than the current two. Not sure if that's worth it for large
// data sets, though.
var resp tc.TrafficDSStatsResponse
if !cfg.ExcludeSummary {
summary, kBs, txns, err := getDSSummary(client, &cfg, inf.Config.ConfigInflux.DSDBName)
if err != nil {
sysErr := fmt.Errorf("Getting summary response from Influx: %v", err)
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, sysErr)
return
}
// match Perl implementation and set summary to zero values if no data
if summary != nil {
resp.Summary = &tc.TrafficDSStatsSummary{
TrafficStatsSummary: *summary,
TotalKiloBytes: kBs,
TotalTransactions: txns,
}
} else {
resp.Summary = &tc.TrafficDSStatsSummary{}
}
}
if !cfg.ExcludeSeries {
series, err := getDSSeries(client, &cfg, inf.Config.ConfigInflux.DSDBName)
if err != nil {
sysErr := fmt.Errorf("Getting summary response from Influx: %v", err)
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, sysErr)
return
}
// match Perl implementation and omit series if no data
if series != nil {
if !cfg.Unix {
series.FormatTimestamps()
}
resp.Series = series
}
}
var respObj struct {
Response interface{} `json:"response"`
}
respObj.Response = resp
respBts, err := json.Marshal(respObj)
if err != nil {
sysErr := fmt.Errorf("Marshalling response: %v", err)
errCode := http.StatusInternalServerError
api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr)
return
}
if cfg.Unix {
w.Header().Set(rfc.ContentType, jsonWithUnixTimestamps.String())
} else {
w.Header().Set(rfc.ContentType, jsonWithRFCTimestamps.String())
}
w.Header().Set(http.CanonicalHeaderKey("vary"), http.CanonicalHeaderKey("Accept"))
api.WriteAndLogErr(w, r, append(respBts, '\n'))
}
func getDSSummary(client *influx.Client, conf *tc.TrafficDSStatsConfig, db string) (*tc.TrafficStatsSummary, *float64, *float64, error) {
qStr := fmt.Sprintf(dsSummaryQuery, db, conf.MetricType)
q := influx.NewQueryWithParameters(qStr,
db,
"rfc3339", //this doesn't actually seem to have any effect...
map[string]interface{}{
"xmlid": conf.DeliveryService,
"start": conf.Start,
"end": conf.End,
"interval": string(conf.Interval),
})
ts, err := getSummary(db, q, client)
if err != nil || ts == nil {
return nil, nil, nil, err
}
var totalKB *float64
var totalTXN *float64
value := float64(ts.Count*60) * ts.Average
if strings.HasPrefix(conf.MetricType, "kbps") {
// TotalBytes is actually in units of kB....
value /= 8
totalKB = &value
} else {
totalTXN = &value
}
return ts, totalKB, totalTXN, nil
}
func dsTenantIDFromXMLID(xmlid string, tx *sql.Tx) (bool, uint, error) {
row := tx.QueryRow(dsTenantIDFromXMLIDQuery, xmlid)
var tid uint
err := row.Scan(&tid)
if err == sql.ErrNoRows {
return false, 0, nil
}
return true, tid, err
}
func getXMLIDFromID(id uint64, tx *sql.Tx) (bool, string, error) {
row := tx.QueryRow(xmlidFromIDQuery, id)
var xmlid string
err := row.Scan(&xmlid)
if err == sql.ErrNoRows {
return false, "", nil
}
return true, xmlid, err
}
func getDSSeries(client *influx.Client, conf *tc.TrafficDSStatsConfig, db string) (*tc.TrafficStatsSeries, error) {
extraClauses := buildExtraClauses(&conf.TrafficStatsConfig)
qStr := fmt.Sprintf(dsSeriesQuery, db, conf.MetricType, conf.Interval, conf.TrafficStatsConfig.OffsetString(), extraClauses)
q := influx.NewQueryWithParameters(qStr,
db,
"rfc3339", // this doesn't seem to do anything...
map[string]interface{}{
"xmlid": conf.DeliveryService,
"start": conf.Start,
"end": conf.End,
})
return getSeries(db, q, client)
}
func findMetric(slice []string, val string) (int, bool) {
for i, item := range slice {
if item == val {
return i, true
}
}
return -1, false
}