diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f00871c6..8595bc1b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Traffic Ops Golang Endpoints - /api/1.1/cachegroupparameters/{{cachegroupID}}/{{parameterID}} `(DELETE)` - /api/1.5/to_extensions `(POST)` + - /api/1.5/stats_summary `(POST)` ### Changed diff --git a/docs/source/api/stats_summary.rst b/docs/source/api/stats_summary.rst index 60423d9d98..5de0568901 100644 --- a/docs/source/api/stats_summary.rst +++ b/docs/source/api/stats_summary.rst @@ -100,6 +100,9 @@ Summary Stats """"""""""""" :cdnName: The CDN name for which the summary stat was taken for + + .. note:: If the ``cdn`` is equal to ``all`` it represents summary_stats across all delivery services across all CDNs + :deliveryServiceName: The :term:`Delivery Service` display name for which the summary stat was taken for .. note:: If the ``deliveryServiceName`` is equal to ``all`` it represents summary_stats across all delivery services within the given CDN @@ -173,4 +176,76 @@ Last Updated Summary Stat { "response": { "summaryTime": "2019-11-19 00:04:06+00" - }} \ No newline at end of file + }} + +``POST`` +======== + +.. versionadded:: 1.5 + +Post a stats summary for a given stat. + +:Auth. Required: Yes +:Roles Required: None +:Response Type: Object + +Request Structure +----------------- +:cdnName: The CDN name for which the summary stat was taken for + + .. note:: If the ``cdn`` is equal to ``all`` it represents summary_stats across all delivery services across all CDNs + +:deliveryServiceName: The :term:`Delivery Service` display name for which the summary stat was taken for + + .. note:: If the ``deliveryServiceName`` is equal to ``all`` it represents summary_stats across all delivery services within the given CDN + +:statName: Stat name summary stat represents +:statValue: Summary stat value +:summaryTime: Timestamp of summary, in an ISO-like format +:statDate: Date stat was taken, in :rfc:`3339` format + +.. note:: ``statName``, ``statValue`` and ``summaryTime`` are required. If ``cdnName`` and ``deliveryServiceName`` are not given they will default to ``all``. + +.. code-block:: http + :caption: Request Example + + POST /api/1.5/stats_summary HTTP/1.1 + Host: trafficops.infra.ciab.test + User-Agent: curl/7.47.0 + Accept: */* + Cookie: mojolicious=... + Content-Length: 113 + Content-Type: application/json + + { + "cdnName": "CDN-in-a-Box", + "deliveryServiceName": "all", + "statName": "daily_maxgbps", + "statValue": 10, + "summaryTime": "2019-12-05 00:03:57+00", + "statDate": "2019-12-05" + } + +Response Structure +------------------ +.. code-block:: http + :caption: Response Example + + HTTP/1.1 200 OK + Access-Control-Allow-Credentials: true + Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie + Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE + Access-Control-Allow-Origin: * + Content-Type: application/json + Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly + Whole-Content-Sha512: ezxk+iP7o7KE7zpWmGc0j8nz5k+1wAzY0HiNiA2xswTQrt+N+6CgQqUV2r9G1HAsPNr0HF2PhYs/Xr7DrYOw0A== + X-Server-Name: traffic_ops_golang/ + Date: Thu, 06 Dec 2018 02:14:45 GMT + Content-Length: 97 + + { "alerts": [ + { + "text": "Stats Summary was successfully created", + "level": "success" + }] + } diff --git a/lib/go-tc/stats_summary.go b/lib/go-tc/stats_summary.go index 9a89a50988..0c1e291485 100644 --- a/lib/go-tc/stats_summary.go +++ b/lib/go-tc/stats_summary.go @@ -1,10 +1,14 @@ package tc import ( + "database/sql" "encoding/json" + "errors" "time" + "github.com/apache/trafficcontrol/lib/go-tc/tovalidate" "github.com/apache/trafficcontrol/lib/go-util" + validation "github.com/go-ozzo/ozzo-validation" ) /* @@ -35,15 +39,22 @@ type StatsSummaryResponse struct { // StatsSummary ... type StatsSummary struct { - ID int `json:"-" db:"id"` - CDNName string `json:"cdnName" db:"cdn_name"` - DeliveryService string `json:"deliveryServiceName" db:"deliveryservice_name"` - StatName string `json:"statName" db:"stat_name"` - StatValue float64 `json:"statValue" db:"stat_value"` + CDNName *string `json:"cdnName" db:"cdn_name"` + DeliveryService *string `json:"deliveryServiceName" db:"deliveryservice_name"` + StatName *string `json:"statName" db:"stat_name"` + StatValue *float64 `json:"statValue" db:"stat_value"` SummaryTime time.Time `json:"summaryTime" db:"summary_time"` StatDate *time.Time `json:"statDate" db:"stat_date"` } +func (ss StatsSummary) Validate(tx *sql.Tx) error { + errs := tovalidate.ToErrors(validation.Errors{ + "statName": validation.Validate(ss.StatName, validation.Required), + "statValue": validation.Validate(ss.StatValue, validation.Required), + }) + return util.JoinErrs(errs) +} + // UnmarshalJSON Customized Unmarshal to force date format on statDate func (ss *StatsSummary) UnmarshalJSON(data []byte) error { type Alias StatsSummary @@ -59,19 +70,30 @@ func (ss *StatsSummary) UnmarshalJSON(data []byte) error { return err } if resp.StatDate != nil { - statDate, err := time.Parse(dateFormat, *resp.StatDate) + statDate, err := parseTime(*resp.StatDate) if err != nil { - return err + return errors.New("invalid timestamp given for statDate") } ss.StatDate = &statDate } - ss.SummaryTime, err = time.Parse(time.RFC3339, resp.SummaryTime) + ss.SummaryTime, err = parseTime(resp.SummaryTime) + if err != nil { + return errors.New("invalid timestamp given for summaryTime") + } + return nil +} + +func parseTime(ts string) (time.Time, error) { + rt, err := time.Parse(time.RFC3339, ts) + if err == nil { + return rt, err + } + rt, err = time.Parse(TimeLayout, ts) if err == nil { - return nil + return rt, err } - ss.SummaryTime, err = time.Parse(TimeLayout, resp.SummaryTime) - return err + return time.Parse(dateFormat, ts) } // MarshalJSON Customized Marshal to force date format on statDate diff --git a/traffic_ops/client/stats_summary.go b/traffic_ops/client/stats_summary.go index ba755208ff..acfe134408 100644 --- a/traffic_ops/client/stats_summary.go +++ b/traffic_ops/client/stats_summary.go @@ -72,6 +72,6 @@ func (to *Session) CreateSummaryStats(statsSummary tc.StatsSummary) (tc.Alerts, if err != nil { return tc.Alerts{}, reqInf, err } - reqInf, err = post(to, fmt.Sprintf("%s/%s", basePathSummaryStats, "create"), reqBody, &alerts) + reqInf, err = post(to, basePathSummaryStats, reqBody, &alerts) return alerts, reqInf, err } diff --git a/traffic_ops/testing/api/v1/stats_summary_test.go b/traffic_ops/testing/api/v1/stats_summary_test.go index 8d6cc563c4..0ae416a8f4 100644 --- a/traffic_ops/testing/api/v1/stats_summary_test.go +++ b/traffic_ops/testing/api/v1/stats_summary_test.go @@ -82,7 +82,7 @@ func GetTestStatsSummaries(t *testing.T) { expectedStatsSummaries: func() []tc.StatsSummary { statsSummaries := []tc.StatsSummary{} for _, ss := range testStatsSummaries { - if ss.StatName == "daily_bytesserved" { + if *ss.StatName == "daily_bytesserved" { statsSummaries = append(statsSummaries, ss) } } @@ -113,13 +113,13 @@ func GetTestStatsSummaries(t *testing.T) { for _, ess := range tc.expectedStatsSummaries { found := false for _, ss := range tsr.Response { - if ess.StatName == ss.StatName && ess.SummaryTime.Equal(ss.SummaryTime) { + if *ess.StatName == *ss.StatName && ess.SummaryTime.Equal(ss.SummaryTime) { found = true break } } if !found { - t.Errorf("expected to find stat %v in stats summary response", ess.StatName) + t.Errorf("expected to find stat %v in stats summary response", *ess.StatName) } } }) @@ -150,8 +150,8 @@ func GetTestStatsSummariesLastUpdated(t *testing.T) { } for _, ss := range testStatsSummaries { testCases = append(testCases, testCase{ - description: fmt.Sprintf("latest updated timestamp for - %v", ss.StatName), - stat: util.StrPtr(ss.StatName), + description: fmt.Sprintf("latest updated timestamp for - %v", *ss.StatName), + stat: ss.StatName, errExpected: false, expectedTimestamp: ss.SummaryTime, }) @@ -175,5 +175,4 @@ func GetTestStatsSummariesLastUpdated(t *testing.T) { } }) } - } diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go index ac257a26cc..78775367f8 100644 --- a/traffic_ops/traffic_ops_golang/routing/routes.go +++ b/traffic_ops/traffic_ops_golang/routing/routes.go @@ -587,6 +587,7 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) { // Stats Summary {1.1, http.MethodGet, `stats_summary/?(\.json)?$`, trafficstats.GetStatsSummary, auth.PrivLevelReadOnly, Authenticated, nil, 380498598, perlBypass}, + {1.5, http.MethodPost, `stats_summary/?(\.json)?$`, trafficstats.CreateStatsSummary, auth.PrivLevelReadOnly, Authenticated, nil, 380491598, noPerlBypass}, // TO Extensions {1.5, http.MethodPost, `to_extensions$`, toextension.CreateTOExtension, auth.PrivLevelReadOnly, Authenticated, nil, 380498599, noPerlBypass}, diff --git a/traffic_ops/traffic_ops_golang/trafficstats/stats_summary.go b/traffic_ops/traffic_ops_golang/trafficstats/stats_summary.go index c86b273d45..4d28fdab58 100644 --- a/traffic_ops/traffic_ops_golang/trafficstats/stats_summary.go +++ b/traffic_ops/traffic_ops_golang/trafficstats/stats_summary.go @@ -111,9 +111,56 @@ func queryStatsSummary(tx *sqlx.Tx, q string, queryValues map[string]interface{} return statsSummaries, nil } +// CreateStatsSummary handler for creating stats summaries +func CreateStatsSummary(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, []string{}, []string{}) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + ss := tc.StatsSummary{} + + if err := api.Parse(r.Body, inf.Tx.Tx, &ss); err != nil { + api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil) + return + } + + // CDN Name and Delivery service name are defaulted to "all" if not defined + if ss.CDNName == nil || len(*ss.CDNName) == 0 { + ss.CDNName = util.StrPtr("all") + } + + if ss.DeliveryService == nil || len(*ss.DeliveryService) == 0 { + ss.DeliveryService = util.StrPtr("all") + } + + id := -1 + rows, err := inf.Tx.NamedQuery(insertQuery(), &ss) + if err != nil { + api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting stats summary: %v", err)) + return + } + for rows.Next() { + if err := rows.Scan(&id); err != nil { + api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning created stats summary id: %v", err)) + return + } + } + if id == -1 { + api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("sstats summary id: %v", err)) + return + } + + successMsg := "Stats Summary was successfully created" + logMsg := fmt.Sprintf("STATSSUMMARY: %v, ID: %v, ACTION: create stats_summary", *ss.StatName, id) + api.CreateChangeLogRawTx(api.ApiChange, logMsg, inf.User, inf.Tx.Tx) + api.WriteRespAlert(w, r, tc.SuccessLevel, successMsg) +} + func selectQuery() string { return `SELECT -id, cdn_name, deliveryservice_name, stat_name, @@ -122,3 +169,22 @@ summary_time, stat_date FROM stats_summary` } + +func insertQuery() string { + return ` +INSERT INTO stats_summary ( + cdn_name, + deliveryservice_name, + stat_name, + stat_value, + summary_time, + stat_date) +VALUES ( + :cdn_name, + :deliveryservice_name, + :stat_name, + :stat_value, + :summary_time, + :stat_date) RETURNING id +` +} diff --git a/traffic_stats/traffic_stats.go b/traffic_stats/traffic_stats.go index 4a1d2d75c1..827fd19395 100644 --- a/traffic_stats/traffic_stats.go +++ b/traffic_stats/traffic_stats.go @@ -315,10 +315,10 @@ func calcDailyMaxGbps(client influx.Client, bp influx.BatchPoints, startTime tim statTime, _ := time.Parse(time.RFC3339, t) log.Infof("max gbps for cdn %v = %v", cdn, value) var statsSummary tc.StatsSummary - statsSummary.CDNName = cdn - statsSummary.DeliveryService = "all" - statsSummary.StatName = "daily_maxgbps" - statsSummary.StatValue = value + statsSummary.CDNName = util.StrPtr(cdn) + statsSummary.DeliveryService = util.StrPtr("all") + statsSummary.StatName = util.StrPtr("daily_maxgbps") + statsSummary.StatValue = util.FloatPtr(value) statsSummary.SummaryTime = time.Now() statsSummary.StatDate = &statTime go writeSummaryStats(config, statsSummary) @@ -375,10 +375,10 @@ func calcDailyBytesServed(client influx.Client, bp influx.BatchPoints, startTime log.Infof("TBytes served for cdn %v = %v", cdn, bytesServedTB) //write to Traffic Ops var statsSummary tc.StatsSummary - statsSummary.CDNName = cdn - statsSummary.DeliveryService = "all" - statsSummary.StatName = "daily_bytesserved" - statsSummary.StatValue = bytesServedTB + statsSummary.CDNName = util.StrPtr(cdn) + statsSummary.DeliveryService = util.StrPtr("all") + statsSummary.StatName = util.StrPtr("daily_bytesserved") + statsSummary.StatValue = util.FloatPtr(bytesServedTB) statsSummary.SummaryTime = time.Now() statsSummary.StatDate = &startTime go writeSummaryStats(config, statsSummary)