This is an automated email from the ASF dual-hosted git repository.

mitchell852 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new f2aeec7  Rewrote deliveryservice_stats to Go (#3758)
f2aeec7 is described below

commit f2aeec7a92d8e88ec2df7114e8ad5e1ef2d63b98
Author: ocket8888 <[email protected]>
AuthorDate: Mon Oct 14 07:45:32 2019 -0600

    Rewrote deliveryservice_stats to Go (#3758)
    
    * Rewrote deliveryservice_stats to Go
    
    * Fixed api.CreateInfluxClient always attempting to use SSL
    
    * Added descriptions of the totalBytes and totalTransactions fields
    
    * fixed influx client builder considering servers that are not ONLINE
    
    * increased traffic_stats log verbosity
    
    * Better error reporting
    
    * added appropriate deprecation notices
    
    * Fixed config key to conform to standard behavior
    
    * fixed GoDoc comments
    
    * Added sane default for 'Secure' flag to influx configs
    
    * Moved MimeType into a dedicated RFC package/library
    
    * returns now explicit
    
    * fixed rfc test compilation
    
    * fixed more implicit returns, match return type of db helper funcs
    
    * s/Equal/Satisfy/g
    
    * match snake_case for configuration variables
    
    * consistent casing for initialisms
    
    * use Ptr util funcs
    
    * var/const blocks
    
    * Move config object into lib/go-tc
    
    * lowercase local variables
    
    * fixed compilation errors introduced by earlier changes
    
    * Split some functionality out into utilities
    
    * Collapse conditionals
    
    * Made checking for excludes more similar to checking for orderables
    
    * All internal errors now straight-up 500s, details to client omitted
    
    * Add test for config parsing
    
    * Switched back to a regular expression for intervals
    
    * Column name value->sum_count
    
    * Added missing struct tag
    
    * Fix erroneous warning log message
---
 CHANGELOG.md                                       |   1 +
 docs/source/api/deliveryservice_stats.rst          | 284 +++++-----
 infrastructure/cdn-in-a-box/traffic_ops/config.sh  |  11 +-
 infrastructure/cdn-in-a-box/traffic_ops/run-go.sh  |   2 +-
 infrastructure/cdn-in-a-box/traffic_stats/run.sh   |   9 +
 lib/go-rfc/mimetype.go                             | 259 +++++++++
 lib/go-rfc/mimetype_test.go                        | 359 +++++++++++++
 lib/go-tc/traffic_stats.go                         | 247 +++++++++
 traffic_ops/app/conf/development/influxdb.conf     |   3 +-
 traffic_ops/app/conf/integration/influxdb.conf     |   3 +-
 traffic_ops/app/conf/production/influxdb.conf      |   3 +-
 traffic_ops/app/conf/test/influxdb.conf            |   3 +-
 traffic_ops/traffic_ops_golang/api/api.go          |  73 +++
 traffic_ops/traffic_ops_golang/config/config.go    |  81 ++-
 traffic_ops/traffic_ops_golang/routing/routes.go   |   4 +
 .../traffic_ops_golang/traffic_ops_golang.go       |   6 +-
 .../trafficstats/trafficstats.go                   | 595 +++++++++++++++++++++
 .../trafficstats/trafficstats_test.go              | 106 ++++
 .../src/common/api/DeliveryServiceStatsService.js  |   6 +-
 19 files changed, 1904 insertions(+), 151 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4b27f2..cb57aa8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
   - /api/1.1/servers/:name/configfiles/ats/parent.config
   - /api/1.1/servers/:name/configfiles/ats/remap.config
   - /api/1.1/user/login/token `POST`
+  - /api/1.4/deliveryservice_stats `GET`
 
 - To support reusing a single riak cluster connection, an optional parameter 
is added to riak.conf: "HealthCheckInterval". This options takes a 'Duration' 
value (ie: 10s, 5m) which affects how often the riak cluster is health checked. 
 Default is currently set to: "HealthCheckInterval": "5s".
 - Added a new Go db/admin binary to replace the Perl db/admin.pl script which 
is now deprecated and will be removed in a future release. The new db/admin 
binary is essentially a drop-in replacement for db/admin.pl since it supports 
all of the same commands and options; therefore, it should be used in place of 
db/admin.pl for all the same tasks.
diff --git a/docs/source/api/deliveryservice_stats.rst 
b/docs/source/api/deliveryservice_stats.rst
index 1bb265b..f920da1 100644
--- a/docs/source/api/deliveryservice_stats.rst
+++ b/docs/source/api/deliveryservice_stats.rst
@@ -19,12 +19,8 @@
 *************************
 ``deliveryservice_stats``
 *************************
-.. caution:: This page is a stub! Much of it may be missing or just downright 
wrong - it needs a lot of love from people with the domain knowledge required 
to update it.
-
 .. versionadded:: 1.2
 
-.. warning:: This endpoint does **NOT** respect tenancy permissions! The bug 
is tracked by `GitHub Issue #3187 
<https://github.com/apache/trafficcontrol/issues/3187>`_.
-
 ``GET``
 =======
 Retrieves time-aggregated statistics on a specific :term:`Delivery Service`.
@@ -37,140 +33,168 @@ Request Structure
 -----------------
 .. table:: Request Query Parameters
 
-       
+----------------------+----------+--------------------------------------------------------------------------------------------------------------------+
-       |    Name              | Required |              Description            
                                                                               |
-       
+======================+==========+====================================================================================================================+
-       | deliveryServiceName  | yes      | The name of the :term:`Delivery 
Service` for which statistics will be aggregated                                
   |
-       
+----------------------+----------+--------------------------------------------------------------------------------------------------------------------+
-       | metricType           | yes      | The metric type being reported - 
one of:                                                                         
  |
-       |                      |          |                                     
                                                                               |
-       |                      |          | kbps                                
                                                                               |
-       |                      |          |   The total traffic rate in 
kilobytes per second served by the :term:`Delivery Service`                     
       |
-       |                      |          | out_bytes                           
                                                                               |
-       |                      |          |   The total number of bytes sent 
out to clients through the :term:`Delivery Service`                             
  |
-       |                      |          | status_4xx                          
                                                                               |
-       |                      |          |   The amount of requests that were 
serviced with 400-499 HTTP status codes                                         
|
-       |                      |          | status_5xx                          
                                                                               |
-       |                      |          |   The amount of requests that were 
serviced with 500-599 HTTP status codes                                         
|
-       |                      |          | tps_total                           
                                                                               |
-       |                      |          |   The total traffic rate in 
transactions per second served by the :term:`Delivery Service`                  
       |
-       |                      |          | tps_2xx                             
                                                                               |
-       |                      |          |   The total traffic rate in 
transactions per second serviced with 200-299 HTTP status codes                 
       |
-       |                      |          | tps_3xx                             
                                                                               |
-       |                      |          |   The total traffic rate in 
transactions per second serviced with 300-399 HTTP status codes                 
       |
-       |                      |          | tps_4xx                             
                                                                               |
-       |                      |          |   The total traffic rate in 
transactions per second serviced with 400-499 HTTP status codes                 
       |
-       |                      |          | tps_5xx                             
                                                                               |
-       |                      |          |   The total traffic rate in 
transactions per second serviced with 500-599 HTTP status codes                 
       |
-       |                      |          |                                     
                                                                               |
-       
+----------------------+----------+--------------------------------------------------------------------------------------------------------------------+
-       | startDate            | yes      | The date and time from which 
statistics shall be aggregated in ISO8601 format, e.g. 
``2018-08-11T12:30:00-07:00``  |
-       
+----------------------+----------+--------------------------------------------------------------------------------------------------------------------+
-       | endDate              | yes      | The date and time until which 
statistics shall be aggregated in ISO8601 format, e.g. 
``2018-08-12T12:30:00-07:00`` |
-       
+----------------------+----------+--------------------------------------------------------------------------------------------------------------------+
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | Name                | Required          | Description                 
                                                                                
                                                                              |
+       
+=====================+===================+===========================================================================================================================================================================================+
+       | deliveryService     | yes\ [#ds-param]_ | Either the :ref:`ds-xmlid` 
of a :term:`Delivery Service` for which statistics will be aggregated or the 
integral, unique identifier of said :term:`Delivery Service`                    
  |
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | deliveryServiceName | yes\ [#ds-param]_ | The :ref:`ds-xmlid` of the 
:term:`Delivery Service` for which statistics will be aggregated                
                                                                               |
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | endDate             | yes               | The date and time until 
which statistics shall be aggregated in :rfc:`3339` format (with or without 
sub-second precision), the number of nanoseconds since the Unix                 
      |
+       |                     |                   | Epoch, or in the same, 
proprietary format as the ``lastUpdated`` fields prevalent throughout the 
Traffic Ops API                                                                 
         |
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | exclude             | no                | Either "series" to omit the 
data series from the result, or "summary" to omit the summary data from the 
result - directly corresponds to fields in the                                  
  |
+       |                     |                   | `Response Structure`_       
                                                                                
                                                                              |
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | interval            | no                | Specifies the interval 
within which data will be "bucketed"; e.g. when requesting data from 
2019-07-25T00:00:00Z to 2019-07-25T23:59:59Z with an interval of "1m",          
              |
+       |                     |                   | the resulting data series 
(assuming it is not excluded) should contain                                    
                                                                                
|
+       |                     |                   | 
:math:`24\frac{\mathrm{hours}}{\mathrm{day}}\times60\frac{\mathrm{minutes}}{\mathrm{hour}}\times1\mathrm{day}\times1\frac{\mathrm{minute}}{\mathrm{data
 point}}=1440\mathrm{data\;points}`|
+       |                     |                   | The allowed values for this 
parameter are valid InfluxQL duration literal strings matching 
:regexp:`^\d+[mhdw]$`                                                           
               |
+       |                     |                   |                             
                                                                                
                                                                              |
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | limit               | no                | A natural number indicating 
the maximum amount of data points should be returned in the ``series`` object   
                                                                              |
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | metricType          | yes               | The metric type being 
reported - one of:                                                              
                                                                                
    |
+       |                     |                   |                             
                                                                                
                                                                              |
+       |                     |                   | kbps                        
                                                                                
                                                                              |
+       |                     |                   |   The total traffic rate in 
kilobytes per second served by the :term:`Delivery Service`                     
                                                                              |
+       |                     |                   | out_bytes                   
                                                                                
                                                                              |
+       |                     |                   |   The total number of bytes 
sent out to clients through the :term:`Delivery Service`                        
                                                                              |
+       |                     |                   | status_4xx                  
                                                                                
                                                                              |
+       |                     |                   |   The amount of requests 
that were serviced with 400-499 HTTP status codes                               
                                                                                
 |
+       |                     |                   | status_5xx                  
                                                                                
                                                                              |
+       |                     |                   |   The amount of requests 
that were serviced with 500-599 HTTP status codes                               
                                                                                
 |
+       |                     |                   | tps_total                   
                                                                                
                                                                              |
+       |                     |                   |   The total traffic rate in 
transactions per second served by the :term:`Delivery Service`                  
                                                                              |
+       |                     |                   | tps_2xx                     
                                                                                
                                                                              |
+       |                     |                   |   The total traffic rate in 
transactions per second serviced with 200-299 HTTP status codes                 
                                                                              |
+       |                     |                   | tps_3xx                     
                                                                                
                                                                              |
+       |                     |                   |   The total traffic rate in 
transactions per second serviced with 300-399 HTTP status codes                 
                                                                              |
+       |                     |                   | tps_4xx                     
                                                                                
                                                                              |
+       |                     |                   |   The total traffic rate in 
transactions per second serviced with 400-499 HTTP status codes                 
                                                                              |
+       |                     |                   | tps_5xx                     
                                                                                
                                                                              |
+       |                     |                   |   The total traffic rate in 
transactions per second serviced with 500-599 HTTP status codes                 
                                                                              |
+       |                     |                   |                             
                                                                                
                                                                              |
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | offset              | no                | A natural number of data 
points to drop from the beginning of the returned data set                      
                                                                                
 |
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | orderby             | no                | Though one struggles to 
imagine why, this can be used to specify "time" to sort data points by their 
"time" (which is the default behavior), or "value" to sort them by their 
"value"     |
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | startDate           | yes               | The date and time from 
which statistics shall be aggregated in :rfc:`3339` format (with or without 
sub-second precision), the number of nanoseconds since the Unix                 
       |
+       |                     |                   | Epoch, or in the same, 
proprietary format as the ``lastUpdated`` fields prevalent throughout the 
Traffic Ops API                                                                 
         |
+       
+---------------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+
+.. _deliveryservice_stats-get-request-example:
+.. code-block:: http
+       :caption: Request Example
+
+       GET 
/api/1.3/deliveryservice_stats?deliveryServiceName=demo1&startDate=2019-07-22T17:55:00Z&endDate=2019-07-22T17:56:00.000Z&metricType=tps_total
 HTTP/1.1
+       User-Agent: python-requests/2.20.1
+       Accept-Encoding: gzip, deflate
+       Accept: application/json;timestamp=unix, 
application/json;timestamp=rfc;q=0.9, application/json;q=0.8, */*;q=0.7
+       Connection: keep-alive
+       Cookie: mojolicious=...
+
+Content Format
+""""""""""""""
+It's important to note in :ref:`deliveryservice_stats-get-request-example` the 
use of a complex "Accept" header. This endpoint accepts two special media types 
in the "Accept" header that instruct it on how to format the timestamps 
associated with the returned data. Specifically, Traffic Ops will recognize the 
special, optional, non-standard parameter of :mimetype:`application/json`: 
``timestamp``. The values of this parameter are restricted to one of
+
+rfc
+       Returned timestamps will be formatted according to :rfc:`3339` (no 
sub-second precision).
+unix
+       Returned timestamps will be formatted as the number of nanoseconds 
since the Unix Epoch (midnight on January 1\ :sup:`st` 1970 UTC).
+
+       .. impl-detail:: The endpoint passes back nanoseconds, specifically, 
because that is the form used both by InfluxDB, which is used to store the data 
being served, and Go's standard library. Clients may need to convert the value 
to match their own standard libraries - e.g. the :js:class:`Date` class in 
Javascript expects milliseconds.
+
+The default behavior - when only e.g. :mimetype:`application/json` or 
:mimetype:`*/*` is given - is to use :rfc:`3339` formatting. It will, however, 
respect quality parameters. It is suggested that clients request timestamps 
they can handle specifically, rather than relying on this default behavior, as 
it **is subject to change** and is in fact **expected to invert in the next 
major release** as string-based time formats become deprecated.
+
+.. seealso:: For more information on the "Accept" HTTP header, consult `its 
dedicated page on MDN 
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept>`_.
 
 Response Structure
 ------------------
-.. table:: Response Keys
-
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       | Parameter                  | Type          | Description              
                                                               |
-       
+============================+===============+=========================================================================================+
-       |``source``                  | string        | The source of the data   
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``summary``                 | hash          | Summary data             
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>totalBytes``             | float         |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>count``                  | int           |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>min``                    | float         |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>max``                    | float         |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>fifthPercentile``        | float         |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>ninetyEighthPercentile`` | float         |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>ninetyFifthPercentile``  | float         |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>average``                | float         |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>totalTransactions``      | int           |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``series``                  | hash          | Series data              
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>count``                  | int           |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>columns``                | array         |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>name``                   | string        |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>values``                 | array         |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>>time``                  | string        |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-       |``>>value``                 | float         |                          
                                                               |
-       
+----------------------------+---------------+-----------------------------------------------------------------------------------------+
-
-.. code-block:: json
+:series: An object containing the actual data series and information necessary 
for working with it.
+
+       :columns: This is an array of names of the columns of the data 
contained in the "values" array - should always be ``["time", "sum_count"]``
+       :count:   The number of data points contained in the "values" array
+       :name:    The name of the data set. Should always match 
:samp:`{metric}.ds.1min` where ``metric`` is the requested ``metricType``
+       :values:  The actual array of data points. Each represents a length of 
time specified by the ``interval`` query parameter
+
+               :time:  The time at which the measurement was taken. This 
corresponds to the *beginning* of the interval. This time comes in the format 
of either an :rfc:`3339`-formatted string, or a number containing the number of 
nanoseconds since the Unix Epoch depending on the "Accept" header sent by the 
client, according to the rules outlined in `Content Format`_.
+               :value: The value of the requested ``metricType`` at the time 
given by ``time``. This will always be a floating point number, unless no data 
is available for the data interval, in which case it will be ``null``
+
+:source:  A legacy field meant only for plugins that override this endpoint to 
name themselves. Should always be "TrafficStats".
+
+       .. deprecated:: 1.4
+               As this has no known purpose, developers are advised it will be 
removed in the future.
+
+:summary: An object containing summary statistics describing the data series
+
+       :average:                The arithmetic mean of the data's values
+       :count:                  The number of measurements taken within the 
requested timespan. This is, in general, **not** the same as the ``count`` 
field of the ``series`` object, as it reflects the number of underlying, 
un-"bucketed" data points, and is therefore dependent on the implementation of 
Traffic Stats.
+       :fifthPercentile:        Data points with values less than or equal to 
this number constitute the "bottom" 5% of the data set
+       :max:                    The maximum value that can be found in the 
requested data set
+       :min:                    The minimum value that can be found in the 
requested data set
+       :ninetyEighthPercentile: Data points with values greater than or equal 
to this number constitute the "top" 2% of the data set
+       :ninetyFifthPercentile:  Data points with values greater than or equal 
to this number constitute the "top" 5% of the data set
+       :totalBytes:             When the ``metricType`` requested is ``kbps``, 
this will contain the total number of bytes transferred by the :term:`Delivery 
Service` within the requested time window. Note that fractional amounts are 
possible, as the data transfer rate will almost certainly not be cleanly 
divided by the requested time range.
+       :totalTransactions:      When the ``metricType`` requested is **not** 
``kbps``, this will contain the total number of transactions completed by the 
:term:`Delivery Service` within the requested time window. Note that fractional 
amounts are possible, as the transaction rate will almost certainly not be 
cleanly divided by the requested time range.
+
+:version: A legacy field that seems to have been meant to indicate the API 
version used. Will always be "1.2"
+
+       .. deprecated:: 1.4
+               As this has no known purpose, developers are advised it will be 
removed in the future.
+
+.. 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-Encoding: gzip
+       Content-Type: application/json
+       Set-Cookie: mojolicious=; Path=/; HttpOnly
+       Whole-Content-Sha512: 
zXJGjcYuu6HxWINVp8HA1gL31J3ukry5wCsTDNxtP/waC6rSD8h10KJ9jEAtRzJ9owOSVPvKaA/2bRu/QeuCpQ==
+       X-Server-Name: traffic_ops_golang/
+       Date: Mon, 22 Jul 2019 17:57:14 GMT
+       Transfer-Encoding: chunked
+
        { "response": {
-               "source": "TrafficStats",
-               "summary": {
-                       "average": 1081172.785,
-                       "count": 28,
-                       "fifthPercentile": 888827.26,
-                       "max": 1326680.31,
-                       "min": 888827.26,
-                       "ninetyEighthPercentile": 1324785.47,
-                       "ninetyFifthPercentile": 1324785.47,
-                       "totalBytes": 37841047.475,
-                       "totalTransactions": 1020202030101
-               },
                "series": {
                        "columns": [
                                "time",
-                               ""
-                       ],
-               "count": 60,
-               "name": "kbps",
-               "tags": {
-                       "cachegroup": "total"
-               },
-               "values": [
-                       [
-                               "2015-08-11T11:36:00Z",
-                               888827.26
-                       ],
-                       [
-                               "2015-08-11T11:37:00Z",
-                               980336.563333333
-                       ],
-                       [
-                               "2015-08-11T11:38:00Z",
-                               952111.975
-                       ],
-                       [
-                               "2015-08-11T11:39:00Z",
-                               null
+                               "sum_count"
                        ],
-                       [
-                               "2015-08-11T11:43:00Z",
-                               null
-                       ],
-                       [
-                               "2015-08-11T11:44:00Z",
-                               934682.943333333
-                       ],
-                       [
-                               "2015-08-11T11:45:00Z",
-                               1251121.28
-                       ],
-                       [
-                               "2015-08-11T11:46:00Z",
-                               1111012.99
+                       "count": 2,
+                       "name": "tps_total.ds.1min",
+                       "tags": {
+                               "cachegroup": "total"
+                       },
+                       "values": [
+                               [
+                                       1563818100000000000,
+                                       0
+                               ],
+                               [
+                                       1563818160000000000,
+                                       0
+                               ]
                        ]
-               ]
-       }}}
+               },
+               "source": "TrafficStats",
+               "summary": {
+                       "average": 0,
+                       "count": 2,
+                       "fifthPercentile": 0,
+                       "max": 0,
+                       "min": 0,
+                       "ninetyEighthPercentile": 0,
+                       "ninetyFifthPercentile": 0,
+                       "totalBytes": null,
+                       "totalTransactions": 0
+               },
+               "version": "1.2"
+       }}
+
+.. [#ds-param] Either ``deliveryServiceName`` or ``deliveryService`` *must* be 
present, but if both are ``deliveryServiceName`` will be used and 
``deliveryService`` will be ignored.
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/config.sh 
b/infrastructure/cdn-in-a-box/traffic_ops/config.sh
index ab0dc41..77b9f1c 100755
--- a/infrastructure/cdn-in-a-box/traffic_ops/config.sh
+++ b/infrastructure/cdn-in-a-box/traffic_ops/config.sh
@@ -128,7 +128,8 @@ cat <<-EOF >/opt/traffic_ops/app/conf/cdn.conf
         "user" : "",
         "password" : "",
         "address" : ""
-    }
+    },
+    "influxdb_conf_location": 
"/opt/traffic_ops/app/conf/production/influx.conf"
 }
 EOF
 
@@ -162,3 +163,11 @@ cat <<-EOF >/opt/traffic_ops/app/conf/production/riak.conf
   "password": "$TV_RIAK_PASSWORD"
 }
 EOF
+
+cat <<-EOF >/opt/traffic_ops/app/conf/production/influx.conf
+{
+    "user": "$INFLUXDB_ADMIN_USER",
+    "password": "$INFLUXDB_ADMIN_PASSWORD",
+    "secure": false
+}
+EOF
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/run-go.sh 
b/infrastructure/cdn-in-a-box/traffic_ops/run-go.sh
index fffe774..1b3d960 100755
--- a/infrastructure/cdn-in-a-box/traffic_ops/run-go.sh
+++ b/infrastructure/cdn-in-a-box/traffic_ops/run-go.sh
@@ -52,7 +52,7 @@ source /to-access.sh
 . /config.sh
 
 while ! nc "$TO_PERL_FQDN" $TO_PERL_PORT </dev/null 2>/dev/null; do
-        echo "waiting for $TO_PERL_FQDN:$TO_PERL_PORT" 
+        echo "waiting for $TO_PERL_FQDN:$TO_PERL_PORT"
         sleep 3
 done
 
diff --git a/infrastructure/cdn-in-a-box/traffic_stats/run.sh 
b/infrastructure/cdn-in-a-box/traffic_stats/run.sh
index cb580f4..31374b2 100755
--- a/infrastructure/cdn-in-a-box/traffic_stats/run.sh
+++ b/infrastructure/cdn-in-a-box/traffic_stats/run.sh
@@ -90,6 +90,15 @@ cat <<-EOF >$TSCONF
 }
 EOF
 
+cat <<-EOF >/opt/traffic_stats/conf/traffic_stats_seelog.xml
+<?xml version='1.0'?>
+<seelog minlevel="debug">
+    <outputs formatid="std:debug-short">
+        <file 
path="/opt/traffic_stats/var/log/traffic_stats/traffic_stats.log" />
+    </outputs>
+</seelog>
+EOF
+
 touch /opt/traffic_stats/var/log/traffic_stats/traffic_stats.log
 
 # Wait for influxdb
diff --git a/lib/go-rfc/mimetype.go b/lib/go-rfc/mimetype.go
new file mode 100644
index 0000000..ad9d056
--- /dev/null
+++ b/lib/go-rfc/mimetype.go
@@ -0,0 +1,259 @@
+package rfc
+
+/*
+ * 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 "mime"
+import "sort"
+import "strings"
+import "strconv"
+
+/*
+MimeType represents a "Media Type" as defined by RFC6838, along with some 
ease-of-use functionality.
+
+Note that this structure is in no way guaranteed to represent a *real* MIME 
Type, only one that
+is *syntactically valid*. The hope is that it will make content negotiation 
easier for developers,
+and should not be considered a security measure by any standard.
+*/
+type MimeType struct {
+       // Name is the full name of the MIME Type, e.g. 'application/json'.
+       // Usually for printing, it's better to call MimeType.String
+       Name string
+       // Parameters contains a map of provided parameter names to 
corresponding values. Note that for
+       // MimeTypes constructed with NewMimeType, this will always be 
initialized, even when empty.
+       Parameters map[string]string
+}
+
+/*
+Quality retrieves and parses the "quality" parameter of a MimeType.
+
+As specified in RFC7231, the quality parameter's name is "q", not actually 
"quality". To obtain
+a literal "quality" parameter value, access MimeType.Parameters directly.
+MimeTypes with no "q" parameter implicitly have a "quality" of 1.0.
+*/
+func (m MimeType) Quality() float64 {
+       if m.Parameters == nil {
+               return 1
+       }
+
+       fs, ok := m.Parameters["q"]
+       if !ok {
+               return 1
+       }
+
+       ret, err := strconv.ParseFloat(fs, 64)
+       if err != nil {
+               return 1
+       }
+       return ret
+}
+
+/*
+Charset retrieves the "charset" parameter of a MimeType.
+
+Returns an empty string if no charset exists in the parameters, or if the 
parameters themselves are
+not initialized.
+*/
+func (m MimeType) Charset() string {
+       if m.Parameters == nil {
+               return ""
+       }
+
+       c, ok := m.Parameters["charset"]
+       if !ok {
+               return ""
+       }
+       return c
+}
+
+// Type returns only the "main" type of a MimeType.
+func (m MimeType) Type() string {
+       return strings.SplitN(m.Name, "/", 2)[0]
+}
+
+// SubType returns only the "sub" type of a MimeType.
+func (m MimeType) SubType() string {
+       s := strings.SplitN(m.Name, "/", 2)
+       if len(s) != 2 {
+               return ""
+       }
+       return s[1]
+}
+
+// Facet returns the MimeType's "facet" if one exists, otherwise an empty 
string.
+func (m MimeType) Facet() string {
+       s := m.SubType()
+       if fx := strings.SplitN(s, ".", 2); len(fx) == 2 {
+               return fx[0]
+       }
+       return ""
+}
+
+// Syntax returns the MimeType's "syntax suffix" if one exists, otherwise an 
empty string.
+func (m MimeType) Syntax() string {
+       s := m.SubType()
+       if fx := strings.Split(s, "+"); len(fx) > 1 {
+               return fx[len(fx)-1]
+       }
+       return ""
+}
+
+// String implements the Stringer interface using mime.FormatMediaType.
+func (m MimeType) String() string {
+       return mime.FormatMediaType(m.Name, m.Parameters)
+}
+
+// Satisfy checks whether or not the MimeType "satifies" some other MimeType, 
o.
+//
+// Note that this does not check if the two are literally the *same*. 
Specifically, if the Type or
+// SubType of the given MimeType o is the special '*' name, then this will 
instead check whether or
+// not this MimeType can *satisfy* the other according to RFC7231. This means 
that this satisfaction
+// check is NOT associative - that is a.Satisfy(b) does not imply b.Satisfy(a).
+//
+// See Also: The MDN documentation on the Accept Header: 
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
+func (m MimeType) Satisfy(o MimeType) bool {
+       // literally anything will satisfy this
+       if o.Type() == "*" && o.SubType() == "*" {
+               return true
+       }
+
+       // it's not syntactically valid to have a "*" type and a non-"*" 
subtype, e.g. "*/foo", so we're
+       // done here
+       if o.Type() != "*" && o.SubType() == "*" {
+               return o.Type() == m.Type()
+       }
+
+       if o.Type() != m.Type() || o.SubType() != m.SubType() {
+               return false
+       }
+
+       for k,v := range o.Parameters {
+               if k == "q" {
+                       continue
+               }
+
+               if mv, ok := m.Parameters[k]; !ok || mv != v {
+                       return false
+               }
+       }
+
+       return true
+}
+
+/*
+Less checks whether or not this MimeType is "less than" some other MimeType, o.
+
+This is done using a comparison of "quality value" of the two MimeTypes, as 
specified in RFC7231.
+
+See Also: The MDN documentation on "quality value" comparisons: 
https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
+*/
+func (m MimeType) Less(o MimeType) bool {
+       mq := m.Quality()
+       oq := o.Quality()
+
+       if mq < oq {
+               return true
+       } else if oq < mq {
+               return false
+       }
+
+       mType := m.Type()
+       mSub := m.SubType()
+       oType := o.Type()
+       oSub := o.SubType()
+
+       if mSub == "*" {
+               if oSub != "*" {
+                       return true
+               }
+
+               if mType == "*" {
+                       if oType != "*" {
+                               return true
+                       }
+               } else if oType == "*" {
+                       return false
+               }
+
+       } else if oSub == "*" {
+               return false
+       }
+
+       return len(m.Parameters) < len(o.Parameters)
+}
+
+/*
+NewMimeType creates a new MimeType, initializing its Parameters map.
+
+If v cannot be parsed as a valid MIME (by mime.ParseMediaType), returns an 
error.
+*/
+func NewMimeType(v string) (m MimeType, err error) {
+       m.Name, m.Parameters, err = mime.ParseMediaType(v)
+       return m, err
+}
+
+/*
+MimeTypesFromAccept constructs a *sorted* list of MimeTypes from the provided 
text, which is assumed
+to be from an HTTP 'Accept' header. The list is sorted using to SortMimeTypes.
+
+If a is an empty string, this will return an empty slice and no error.
+*/
+func MimeTypesFromAccept(a string) ([]MimeType, error) {
+       mimes := []MimeType{}
+       if a == "" {
+               return mimes, nil
+       }
+
+       for _, raw := range strings.Split(a, ",") {
+               m, err := NewMimeType(raw)
+               if err != nil {
+                       return mimes, err
+               }
+               mimes = append(mimes, m)
+       }
+       SortMimeTypes(mimes)
+       return mimes, nil
+}
+
+/*
+SortMimeTypes sorts the passed MimeTypes according to their "quality value". 
See MimeType.Less for
+more information on how MimeTypes are compared.
+*/
+func SortMimeTypes(m []MimeType) {
+       // using !Less because default sort order is ascending
+       sort.SliceStable(m, func(i, j int) bool {return !m[i].Less(m[j])})
+}
+
+// MIME_JSON is a pre-defined MimeType for JSON data.
+var MIME_JSON = MimeType {
+       Name: "application/json",
+       Parameters: map[string]string{},
+}
+
+// MIME_PLAINTEXT is a pre-defined MimeType for plain text data.
+var MIME_PLAINTEXT = MimeType {
+       Name: "text/plain",
+       Parameters: map[string]string{"charset": "utf-8"},
+}
+
+// MIME_HTML is a pre-defined MimeType for HTML data.
+var MIME_HTML = MimeType {
+       Name: "text/html",
+       Parameters: map[string]string{"charset": "utf-8"},
+}
diff --git a/lib/go-rfc/mimetype_test.go b/lib/go-rfc/mimetype_test.go
new file mode 100644
index 0000000..afd5737
--- /dev/null
+++ b/lib/go-rfc/mimetype_test.go
@@ -0,0 +1,359 @@
+package rfc
+
+/*
+ * 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 "fmt"
+import "testing"
+
+const TEST_MIME = "tEXt/vND.plaIN+cRLf;    charset=utf-8; q=2.2;foo=bar"
+const PRETTY_MIME = "text/vnd.plain+crlf; charset=utf-8; foo=bar; q=2.2"
+
+func ExampleNewMimeType() {
+       m, err := NewMimeType("text/plain;charset=utf-8")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+
+       fmt.Println("Name:", m.Name, "Parameters:", m.Parameters)
+       // Output: Name: text/plain Parameters: map[charset:utf-8]
+}
+
+func ExampleMimeType_Quality() {
+       m, err := NewMimeType("text/plain;charset=utf-8")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+       q := m.Quality()
+       fmt.Print(q)
+
+       m.Parameters["q"] = "0.9"
+       q = m.Quality()
+       fmt.Println("", q)
+
+       // Output: 1 0.9
+}
+
+func ExampleMimeType_Charset() {
+       m, err := NewMimeType("text/plain;charset=utf-8")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+       c,_ := m.Charset() // it's okay to ignore this error, because I was a 
good boy and used NewMimeType
+       fmt.Println(c)
+       // Output: utf-8
+}
+
+func ExampleMimeType_Type() {
+       m, err := NewMimeType("text/plain;charset=utf-8")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+       fmt.Println(m.Type())
+       // Output: text
+}
+
+func ExampleMimeType_SubType() {
+       m, err := NewMimeType("text/vnd.plain+crlf;charset=utf-8")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+       fmt.Println(m.SubType())
+       // Output: vnd.plain+crlf
+}
+
+func ExampleMimeType_Facet() {
+       m, err := NewMimeType("text/vnd.plain+crlf;charset=utf-8")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+       fmt.Println(m.Facet())
+       // Output: vnd
+}
+
+func ExampleMimeType_Syntax() {
+       m, err := NewMimeType("text/vnd.plain+crlf;charset=utf-8")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+       fmt.Println(m.Syntax())
+       // Output: crlf
+}
+
+func ExampleMimeType_String() {
+       m, err := NewMimeType("text/plain;foo=bar;charset=utf-8")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+       fmt.Println(m)
+       // Output: text/plain; charset=utf-8; foo=bar
+}
+
+func ExampleMimeType_Satisfy() {
+       m, err := NewMimeType("text/plain")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+
+       o, err := NewMimeType("text/*")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+
+       fmt.Println(m.Satisfy(o), o.Satisfy(m))
+       // Output: true false
+}
+
+func ExampleMimeType_Less() {
+       one, err := NewMimeType("text/plain")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+
+       two, err := NewMimeType("text/*")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+
+       three, err := NewMimeType("text/plain;q=0.9")
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+
+
+       fmt.Println(one.Less(two), two.Less(three), one.Less(three))
+       // Output: false false false
+}
+
+func ExampleMimeTypesFromAccept() {
+       const acceptLine = "text/html,text/xml;q=0.9,text/*;q=0.9,*/*"
+       mimes, err := MimeTypesFromAccept(acceptLine)
+       if err != nil {
+               fmt.Println(err.Error())
+               return
+       }
+
+       for _,m := range mimes {
+               fmt.Printf("%s, ", m)
+       }
+       fmt.Println()
+       // Output: text/html, */*, text/xml; q=0.9, text/*; q=0.9,
+}
+
+func ExampleSortMimeTypes() {
+       // Normally don't do this, but for the sake of brevity in an example I 
will
+       mimes := []MimeType{
+               MimeType {
+                       "text/html",
+                       map[string]string{},
+               },
+               MimeType {
+                       "text/xml",
+                       map[string]string{"q": "0.9"},
+               },
+               MimeType {
+                       "text/*",
+                       map[string]string{"q": "0.9"},
+               },
+               MimeType {
+                       "*/*",
+                       map[string]string{},
+               },
+       }
+
+       SortMimeTypes(mimes)
+       for _,m := range mimes {
+               fmt.Printf("%s, ", m)
+       }
+       fmt.Println()
+       // Output: text/html, */*, text/xml; q=0.9, text/*; q=0.9,
+}
+
+func TestMimeType(t *testing.T) {
+       m, err := NewMimeType(TEST_MIME)
+       if err != nil {
+               t.Fatalf("Failed to construct a MimeType from TEST_MIME: %v", 
err)
+       }
+
+       if m.Name != "text/vnd.plain+crlf" {
+               t.Errorf("Incorrect MIME name, expected 'text/vnd.plain+crld' 
but got '%s'", m.Name)
+       }
+
+       if m.Type() != "text" {
+               t.Errorf("Incorrect 'Type', expected 'text' but got '%s'", 
m.Type())
+       }
+
+       if m.SubType() != "vnd.plain+crlf" {
+               t.Errorf("Incorrect 'SubType', expected 'vnd.plain+crlf' but 
got '%s'", m.SubType())
+       }
+
+       if m.Facet() != "vnd" {
+               t.Errorf("Incorrect 'Facet', expected 'vnd' but got '%s'", 
m.Facet())
+       }
+
+       if m.Syntax() != "crlf" {
+               t.Errorf("Incorrect 'syntax suffix', expected 'crlf' but got 
'%s'", m.Syntax())
+       }
+
+       if m.String() != PRETTY_MIME {
+               t.Errorf("Incorrect string representation, expected '%s' but 
got '%s'", PRETTY_MIME, m.String())
+       }
+
+       if len(m.Parameters) != 3 {
+               t.Errorf("Incorrect number of Parameters, expected 3 but got 
%d", len(m.Parameters))
+       }
+
+       if q := m.Quality(); q != 2.2 {
+               t.Errorf("Incorrect quality, expected 2.2 but got %g", q)
+       }
+
+       if c := m.Charset(); c != "utf-8" {
+               t.Errorf("Incorrect charset, expected 'utf-8', but got '%s'", c)
+       }
+
+}
+
+func TestMimeType_Satisfy(t *testing.T) {
+       m, err := NewMimeType(TEST_MIME)
+       if err != nil {
+               t.Fatalf("Failed to construct a MimeType from TEST_MIME: %v", 
err)
+       }
+
+       o, err := NewMimeType("*/*")
+       if err != nil {
+               t.Fatalf("Failed to construct a MimeType from '*/*': %v", err)
+       }
+
+       if !m.Satisfy(o) {
+               t.Errorf("Expected %s to satisfy %s, but it did not", m, o)
+       }
+
+       if o.Satisfy(m) {
+               t.Errorf("Expected %s to not satisfy %s, but it did", o, m)
+       }
+
+       if o, err = NewMimeType("text/*"); err != nil {
+               t.Fatalf("Failed to construct a MimeType from 'text/*': %v", 
err)
+       }
+
+       if !m.Satisfy(o) {
+               t.Errorf("Expected %s to satisfy %s, but it did not", m, o)
+       }
+
+       if o.Satisfy(m) {
+               t.Errorf("Expected %s to not satisfy %s, but it did", o, m)
+       }
+
+       if o, err = NewMimeType("text/vnd.plain+crlf;q=2.1"); err != nil {
+               t.Fatalf("Failed to construct a MimeType from 
'text/vnd.plain+crlf;q=2.1': %v", err)
+       }
+
+       if !m.Satisfy(o) {
+               t.Errorf("Expected %s to satisfy %s, but it did not", m, o)
+       }
+
+       if o.Satisfy(m) {
+               t.Errorf("Expected %s to not satisfy %s, but it did", o, m)
+       }
+}
+
+func TestMimeType_Less(t *testing.T) {
+       m, err := NewMimeType("text/*")
+       if err != nil {
+               t.Fatalf("Failed to construct MimeType from 'text/*': %v", err)
+       }
+
+       o, err := NewMimeType("*/*")
+       if err != nil {
+               t.Fatalf("Failed to construct MimeType from '*/*': %v", err)
+       }
+
+       const less = "Expected %s to be less than %s, but it was not"
+       const notLess = "Expected %s to not be less than %s, but it was"
+
+       if m.Less(o) {
+               t.Errorf(notLess, m, o)
+       }
+
+       if !o.Less(m) {
+               t.Errorf(less, o, m)
+       }
+
+       if o, err = NewMimeType("text/plain"); err != nil {
+               t.Fatalf("Failed to construct MimeType from 'text/plain': %v", 
err)
+       }
+
+       if !m.Less(o) {
+               t.Errorf(less, m, o)
+       }
+
+       if o.Less(m) {
+               t.Errorf(notLess, o, m)
+       }
+
+       if m, err = NewMimeType("text/plain;foo=bar;q=1.0"); err != nil {
+               t.Fatalf("Failed to construct MimeType from 
'text/plain;foo=bar;q=1.0': %v", err)
+       }
+
+       if m.Less(o) {
+               t.Errorf(notLess, m, o)
+       }
+
+       if !o.Less(m) {
+               t.Errorf(less, o, m)
+       }
+
+       if o, err = NewMimeType("text/plain;q=1.1"); err != nil {
+               t.Fatalf("Failed to construct MimeType from 'text/plain;q=1.1': 
%v", err)
+       }
+
+       if !m.Less(o) {
+               t.Errorf(less, m, o)
+       }
+
+       if o.Less(m) {
+               t.Errorf(notLess, o, m)
+       }
+
+       if o, err = NewMimeType("text/plain;fizz=buzz;q=1.0"); err != nil {
+               t.Fatalf("Failed to construct MimeType from 
'text/plain;fizz=buzz;q=1.0': %v", err)
+       }
+
+       if m.Less(o) {
+               t.Errorf(notLess, m, o)
+       }
+
+       if o.Less(m) {
+               t.Errorf(notLess, o, m)
+       }
+
+}
diff --git a/lib/go-tc/traffic_stats.go b/lib/go-tc/traffic_stats.go
new file mode 100644
index 0000000..a4f0573
--- /dev/null
+++ b/lib/go-tc/traffic_stats.go
@@ -0,0 +1,247 @@
+package tc
+
+/*
+ * 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 "encoding/json"
+import "errors"
+import "fmt"
+import "regexp"
+import "strconv"
+import "strings"
+import "time"
+
+import "github.com/apache/trafficcontrol/lib/go-log"
+
+import influx "github.com/influxdata/influxdb1-client/v2"
+
+// TRAFFIC_STATS_VERSION was supposed to be the "API version", but actually 
the plugin (this route
+// used to be a plugin in Perl) always returned this static value
+const TRAFFIC_STATS_VERSION = "1.2"
+
+// TRAFFIC_STATS_SOURCE is the value of the "source" field in an API response. 
Perl always returned
+// source="TrafficStats", so we do too
+const TRAFFIC_STATS_SOURCE = "TrafficStats"
+
+// TrafficStatsDurationPattern reflects all the possible durations that can be 
requested via the
+// deliveryservice_stats endpoint
+var TrafficStatsDurationPattern = regexp.MustCompile(`^\d+[mhdw]$`)
+
+// DurationLiteralToSeconds returns the number of seconds to which an InfluxQL 
duration literal is
+// equivalent. For invalid objects, it returns -1 - otherwise it will always, 
of course be > 0.
+func DurationLiteralToSeconds(d string) (int64, error) {
+       if strings.HasSuffix(d, "m") {
+               v,err := strconv.ParseInt(strings.Split(d, "m")[0], 10, 64)
+               return v*60, err
+       }
+
+       if strings.HasSuffix(d, "h") {
+               v,err := strconv.ParseInt(strings.Split(d, "h")[0], 10, 64)
+               return v*3600, err
+       }
+
+       if strings.HasSuffix(d, "d") {
+               v,err := strconv.ParseInt(strings.Split(d, "d")[0], 10, 64)
+               return v*86400, err
+       }
+
+       if strings.HasSuffix(d, "w") {
+               v,err := strconv.ParseInt(strings.Split(d, "w")[0], 10, 64)
+               return v*604800, err
+       }
+
+       return -1, errors.New("Invalid duration literal, no recognized suffix")
+}
+
+// TrafficStatsOrderable encodes what columns by which the data returned from 
a Traffic Stats query
+// may be ordered.
+type TrafficStatsOrderable string
+const (
+       // TimeOrder indicates an ordering by time at which the measurement was 
taken
+       TimeOrder  TrafficStatsOrderable = "time"
+       // ValueOrder indicates an ordering by the actual value of the 
measurement
+       ValueOrder TrafficStatsOrderable = "sum_count"
+)
+
+// OrderableFromString parses the passed string and returns the corresponding 
value as a pointer to
+// a TrafficStatsOrderable - or nil if the value was invalid.
+func OrderableFromString(v string) *TrafficStatsOrderable {
+       var o TrafficStatsOrderable
+       switch v {
+       case "time":
+               o = TimeOrder
+       case "value":
+               o = ValueOrder
+       default:
+               return nil
+       }
+       return &o
+}
+
+// TrafficStatsExclude encodes what parts of a response to a request to a 
"Traffic Stats" endpoint
+// of the TO API may be omitted.
+type TrafficStatsExclude string
+const (
+       ExcludeSeries TrafficStatsExclude = "series"
+       ExcludeSummary TrafficStatsExclude = "summary"
+       ExcludeInvalid TrafficStatsExclude = "INVALID"
+)
+
+// ExcludeFromString parses the passed string and returns the corresponding 
value as a TrafficStatsExclude.
+func ExcludeFromString(v string) TrafficStatsExclude {
+       switch v {
+       case "series":
+               return ExcludeSeries
+       case "summary":
+               return ExcludeSummary
+       default:
+               return ExcludeInvalid
+       }
+}
+
+
+// TrafficStatsConfig represents the configuration of a request made to 
Traffic Stats. This is
+// typically constructed by parsing a request body submitted to Traffic Ops.
+type TrafficStatsConfig struct {
+       DeliveryService string
+       End             time.Time
+       ExcludeSeries   bool
+       ExcludeSummary  bool
+       Interval        string
+       Limit           *uint64
+       MetricType      string
+       Offset          *uint64
+       OrderBy         *TrafficStatsOrderable
+       Start           time.Time
+       Unix            bool
+}
+
+// This is a stupid, dirty hack to try to convince Influx to not give back 
data that's outside of the
+// range in a WHERE clause. It doesn't work, but it helps.
+// (https://github.com/influxdata/influxdb/issues/8010)
+func (c *TrafficStatsConfig) OffsetString() string {
+       iSecs, err := DurationLiteralToSeconds(c.Interval)
+       if err != nil {
+               log.Errorf("Parsing duration literal: %v", err)
+               return "0s"
+       }
+       return fmt.Sprintf("%ds", int64(c.Start.Sub(time.Unix(0, 
0))/time.Second)%iSecs)
+}
+
+// TrafficStatsResponse represents a response from one of the "Traffic Stats 
endpoints" of the
+// Traffic Ops API, e.g. `/deliveryservice_stats`.
+type TrafficStatsResponse struct {
+       // Series holds the actual data - it is NOT in general the same as a 
github.com/influxdata/influxdb1-client/models.Row
+       Series *TrafficStatsSeries `json:"series,omitempty"`
+       // Source has an unknown purpose. I believe this is supposed to name 
the "plugin" that provided
+       // the data - kept for compatibility with the Perl version(s) of the 
"Traffic Stats endpoints".
+       // Deprecated: this'll be removed or reworked to make more sense in the 
future
+       Source string `json:"source"`
+       // Summary contains summary statistics of the data in Series
+       Summary *TrafficStatsSummary `json:"summary,omitempty"`
+       // Version is supposed to represent the API version - but actually the 
API just reports a static
+       // number (TRAFFIC_STATS_VERSION).
+       // Deprecated: this'll be removed or reworked to make more sense in the 
future
+       Version string `json:"version"`
+}
+
+// TrafficStatsSummary contains summary statistics for a data series.
+type TrafficStatsSummary struct {
+       // Average is calculated as an arithmetic mean
+       Average float64 `json:"average"`
+       // Count is the total number of data points _except_ for any values 
that would appear as 'nil'
+       // in the corresponding series
+       Count                  uint    `json:"count"`
+       FifthPercentile        float64 `json:"fifthPercentile"`
+       Max                    float64 `json:"max"`
+       Min                    float64 `json:"min"`
+       NinetyEighthPercentile float64 `json:"ninetyEighthPercentile"`
+       NinetyFifthPercentile  float64 `json:"ninetyFifthPercentile"`
+       // TotalBytes is the total number of bytes served when the "metric 
type" requested is "kbps"
+       // (or actually just contains "kbps"). If this is not nil, 
TotalTransactions *should* always be
+       // nil.
+       TotalBytes *float64 `json:"totalBytes"`
+       // Totaltransactions is the total number of transactions within the 
requested window. Whenever
+       // the requested metric doesn't contain "kbps", it assumed to be some 
kind of transactions
+       // measurement. In that case, this will not be nil - otherwise it will 
be nil. If this not nil,
+       // TotalBytes *should* always be nil.
+       TotalTransactions *float64 `json:"totalTransactions"`
+}
+
+// TrafficStatsSeries is the actual data returned by a request to a "Traffic 
Stats endpoint".
+type TrafficStatsSeries struct {
+       // Columns is a list of column names. Each "row" in Values is ordered 
to match up with these
+       // column names.
+       Columns []string `json:"columns"`
+       // Count is the total number of returned data points. Should be the 
same as len(Values)
+       Count uint `json:"count"`
+       // Name is the name of the InfluxDB database from which the data was 
retrieved
+       Name string `json:"name"`
+       // Tags is a set of InfluxDB tags associated with the requested 
database.
+       Tags map[string]string `json:"tags"`
+       // Values is an array of rows of arbitrary data that can only really be 
interpreted by
+       // inspecting Columns, in general. In practice, however, each element 
is nearly always a
+       // slice where the first element is an RFC3339 timestamp (as a string) 
and the second/final
+       // element is a floating point number (or nil) indicating the value at 
that time.
+       Values [][]interface{} `json:"values"`
+}
+
+// FormatTimestamps formats the timestamps contained in the Values array as 
RFC3339 strings.
+// This returns an error if the data is not in the expected format.
+func (s TrafficStatsSeries) FormatTimestamps() error {
+       for i, v := range s.Values {
+               if len(v) != 2 {
+                       return fmt.Errorf("Datapoint %d (%v) malformed", i, v)
+               }
+
+               switch v[0].(type) {
+               case int64:
+                       s.Values[i][0] = time.Unix(0, 
v[0].(int64)).Format(time.RFC3339)
+               case float64:
+                       s.Values[i][0] = time.Unix(0, 
int64(v[0].(float64))).Format(time.RFC3339)
+               case json.Number:
+                       val, err := v[0].(json.Number).Int64()
+                       if err != nil {
+                               return fmt.Errorf("Datapoint %d (%v) malformed: 
%v", i, v, err)
+                       }
+                       s.Values[i][0] = time.Unix(0, val).Format(time.RFC3339)
+               default:
+                       return fmt.Errorf("Invalid type %T for datapoint %d 
(%v)", v[0], i, v)
+               }
+       }
+       return nil
+}
+
+// MessagesToString converts a set of messages from an InfluxDB node into a 
single, print-able string
+func MessagesToString (msgs []influx.Message) string {
+       if msgs == nil || len(msgs) == 0 {
+               return ""
+       }
+
+       b := strings.Builder{}
+       b.Write([]byte("Messages: ["))
+       for _, m := range msgs {
+               b.WriteString(m.Level)
+               b.WriteRune(':')
+               b.WriteString(m.Text)
+               b.Write([]byte(", "))
+       }
+       b.WriteRune(']')
+       return b.String()
+}
diff --git a/traffic_ops/app/conf/development/influxdb.conf 
b/traffic_ops/app/conf/development/influxdb.conf
index 4e1f78a..4bd5a91 100644
--- a/traffic_ops/app/conf/development/influxdb.conf
+++ b/traffic_ops/app/conf/development/influxdb.conf
@@ -2,5 +2,6 @@
    "user" : "influxuser",
    "password" : "password",
    "deliveryservice_stats_db_name":"deliveryservice_stats",
-   "cache_stats_db_name":"cache_stats"
+   "cache_stats_db_name":"cache_stats",
+   "secure": false
 }
diff --git a/traffic_ops/app/conf/integration/influxdb.conf 
b/traffic_ops/app/conf/integration/influxdb.conf
index 43c076e..3091d3a 100644
--- a/traffic_ops/app/conf/integration/influxdb.conf
+++ b/traffic_ops/app/conf/integration/influxdb.conf
@@ -1,4 +1,5 @@
 {
        "user": "influxuser",
-       "password": "password"
+       "password": "password",
+       "secure": false
 }
diff --git a/traffic_ops/app/conf/production/influxdb.conf 
b/traffic_ops/app/conf/production/influxdb.conf
index 9137c75..ad991d6 100644
--- a/traffic_ops/app/conf/production/influxdb.conf
+++ b/traffic_ops/app/conf/production/influxdb.conf
@@ -2,5 +2,6 @@
        "user": "influxuser",
        "password": "password",
        "deliveryservice_stats_db_name": "deliveryservice_stats",
-       "cache_stats_db_name": "cache_stats"
+       "cache_stats_db_name": "cache_stats",
+       "secure": false
 }
diff --git a/traffic_ops/app/conf/test/influxdb.conf 
b/traffic_ops/app/conf/test/influxdb.conf
index 4e1f78a..4bd5a91 100644
--- a/traffic_ops/app/conf/test/influxdb.conf
+++ b/traffic_ops/app/conf/test/influxdb.conf
@@ -2,5 +2,6 @@
    "user" : "influxuser",
    "password" : "password",
    "deliveryservice_stats_db_name":"deliveryservice_stats",
-   "cache_stats_db_name":"cache_stats"
+   "cache_stats_db_name":"cache_stats",
+   "secure": false
 }
diff --git a/traffic_ops/traffic_ops_golang/api/api.go 
b/traffic_ops/traffic_ops_golang/api/api.go
index 1e91890..83958cb 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -41,6 +41,7 @@ import (
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tocookie"
 
+       influx "github.com/influxdata/influxdb1-client/v2"
        "github.com/jmoiron/sqlx"
        "github.com/lib/pq"
 )
@@ -50,6 +51,18 @@ const ConfigContextKey = "context"
 const ReqIDContextKey = "reqid"
 const APIRespWrittenKey = "respwritten"
 
+const influxServersQuery = `
+SELECT (host_name||'.'||domain_name) as fqdn,
+       tcp_port,
+       https_port
+FROM server
+WHERE type in ( SELECT id
+                FROM type
+                WHERE name='INFLUXDB'
+              )
+AND status=(SELECT id FROM status WHERE name='ONLINE')
+`
+
 // WriteResp takes any object, serializes it as JSON, and writes that to w. 
Any errors are logged and written to w via tc.GetHandleErrorsFunc.
 // This is a helper for the common case; not using this in unusual cases is 
perfectly acceptable.
 func WriteResp(w http.ResponseWriter, r *http.Request, v interface{}) {
@@ -406,6 +419,66 @@ func SendMail(to rfc.EmailAddress, msg []byte, cfg 
*config.Config) (int, error,
        return http.StatusOK, nil, nil
 }
 
+// CreateInfluxClient onstructs and returns an InfluxDB HTTP client, if 
enabled and when possible.
+// The error this returns should not be exposed to the user; it's for logging 
purposes only.
+//
+// If Influx connections are not enabled, this will return `nil` - but also no 
error. It is expected
+// that the caller will handle this situation appropriately.
+func (inf *APIInfo) CreateInfluxClient() (*influx.Client, error) {
+       if !inf.Config.InfluxEnabled {
+               return nil, nil
+       }
+
+       var fqdn string
+       var tcpPort uint
+       var httpsPort sql.NullInt64 // this is the only one that's optional
+
+       row := inf.Tx.Tx.QueryRow(influxServersQuery)
+       if e := row.Scan(&fqdn, &tcpPort, &httpsPort); e != nil {
+               return nil, fmt.Errorf("Failed to create influx client: %v", e)
+       }
+
+       host := "http%s://%s:%d"
+       if inf.Config.ConfigInflux != nil && *inf.Config.ConfigInflux.Secure {
+               if !httpsPort.Valid {
+                       log.Warnf("INFLUXDB Server %s has no secure ports, 
assuming default of 8086!", fqdn)
+                       httpsPort = sql.NullInt64{8086, true}
+               }
+               port, err := httpsPort.Value()
+               if err != nil {
+                       return nil, fmt.Errorf("Failed to create influx client: 
%v", err)
+               }
+
+               p := port.(int64)
+               if p <= 0 || p > 65535 {
+                       log.Warnf("INFLUXDB Server %s has invalid port, 
assuming default of 8086!", fqdn)
+                       p = 8086
+               }
+
+               host = fmt.Sprintf(host, "s", fqdn, p)
+       } else if tcpPort > 0 && tcpPort <= 65535 {
+               host = fmt.Sprintf(host, "", fqdn, tcpPort)
+       } else {
+               log.Warnf("INFLUXDB Server %s has invalid port, assuming 
default of 8086!", fqdn)
+               host = fmt.Sprintf(host, "", fqdn, 8086)
+       }
+
+       config := influx.HTTPConfig{
+               Addr:      host,
+               Username:  inf.Config.ConfigInflux.User,
+               Password:  inf.Config.ConfigInflux.Password,
+               UserAgent: fmt.Sprintf("TrafficOps/%s (Go)", 
inf.Config.Version),
+               Timeout:   time.Duration(float64(inf.Config.ReadTimeout)/2.1) * 
time.Second,
+       }
+
+       var client influx.Client
+       client, e := influx.NewHTTPClient(config)
+       if client == nil {
+               return nil, fmt.Errorf("Failed to create influx client (client 
was nil): %v", e)
+       }
+       return &client, e
+}
+
 // APIInfoImpl implements APIInfo via the APIInfoer interface
 type APIInfoImpl struct {
        ReqInfo *APIInfo
diff --git a/traffic_ops/traffic_ops_golang/config/config.go 
b/traffic_ops/traffic_ops_golang/config/config.go
index 14dae49..d5d254a 100644
--- a/traffic_ops/traffic_ops_golang/config/config.go
+++ b/traffic_ops/traffic_ops_golang/config/config.go
@@ -21,16 +21,17 @@ package config
 
 import (
        "encoding/json"
+       "errors"
        "fmt"
        "io/ioutil"
        "net/url"
        "os"
        "path/filepath"
-
        "strings"
 
        "github.com/apache/trafficcontrol/lib/go-log"
        "github.com/apache/trafficcontrol/lib/go-rfc"
+       "github.com/apache/trafficcontrol/lib/go-util"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
        "github.com/basho/riak-go-client"
 )
@@ -47,12 +48,15 @@ type Config struct {
        DB                     ConfigDatabase `json:"db"`
        Secrets                []string       `json:"secrets"`
        // NOTE: don't care about any other fields for now..
-       RiakAuthOptions *riak.AuthOptions
-       RiakEnabled     bool
-       ConfigLDAP      *ConfigLDAP
-       LDAPEnabled     bool
-       LDAPConfPath    string `json:"ldap_conf_location"`
-       Version         string
+       RiakAuthOptions  *riak.AuthOptions
+       RiakEnabled      bool
+       ConfigLDAP       *ConfigLDAP
+       LDAPEnabled      bool
+       LDAPConfPath     string `json:"ldap_conf_location"`
+       ConfigInflux     *ConfigInflux
+       InfluxEnabled    bool
+       InfluxDBConfPath string `json:"influxdb_conf_path"`
+       Version          string
 }
 
 // ConfigHypnotoad carries http setting for hypnotoad (mojolicious) server
@@ -141,6 +145,14 @@ type ConfigLDAP struct {
        LDAPTimeoutSecs int    `json:"ldap_timeout_secs"`
 }
 
+type ConfigInflux struct {
+       User        string `json:"user"`
+       Password    string `json:"password"`
+       DSDBName    string `json:"deliveryservice_stats_db_name"`
+       CacheDBName string `json:"cache_stats_db_name"`
+       Secure      *bool  `json:"secure"`
+}
+
 const DefaultLDAPTimeoutSecs = 60
 const DefaultDBQueryTimeoutSecs = 20
 
@@ -235,11 +247,25 @@ func LoadConfig(cdnConfPath string, dbConfPath string, 
riakConfPath string, appV
                                return cfg, []error{err}, BlockStartup
                        }
                } else {
-                       cfg.LDAPEnabled = false
-                       return cfg, []error{}, AllowStartup // no ldap.conf, 
disable and allow startup
+                       cfg.LDAPEnabled = false // no ldap.conf, disable and 
allow startup
                }
        }
 
+       idbPath := cfg.InfluxDBConfPath
+       if idbPath == "" {
+               idbPath = filepath.Join(filepath.Dir(cdnConfPath), 
"influxdb.conf")
+       }
+
+       if _, err = os.Stat(idbPath); err != nil {
+               if os.IsNotExist(err) {
+                       cfg.InfluxEnabled = false
+               } else {
+                       return cfg, []error{err}, BlockStartup
+               }
+       } else if cfg.InfluxEnabled, cfg.ConfigInflux, err = 
GetInfluxConfig(idbPath); err != nil {
+               return cfg, []error{err}, BlockStartup
+       }
+
        return cfg, []error{}, AllowStartup
 }
 
@@ -381,6 +407,43 @@ func GetLDAPConfig(LDAPConfPath string) (bool, 
*ConfigLDAP, error) {
        return true, LDAPconf, nil
 }
 
+func GetInfluxConfig(path string) (bool, *ConfigInflux, error) {
+       raw, err := ioutil.ReadFile(path)
+       if err != nil {
+               return false, nil, fmt.Errorf("reading InfluxDB configuration 
from '%s': %v", path, err)
+       }
+
+       c := ConfigInflux{}
+       if err := json.Unmarshal(raw, &c); err != nil {
+               return false, nil, fmt.Errorf("parsing InfluxDB configuration 
from '%s': %v", path, err)
+       }
+
+       if c.User == "" {
+               return false, &c, errors.New("InfluxDB configuration missing a 
username")
+       }
+
+       if c.Password == "" {
+               return false, &c, errors.New("InfluxDB configuration missing a 
password")
+       }
+
+       if c.DSDBName == "" {
+               log.Warnln("InfluxDB configuration does not specify a DS stats 
DB name - falling back on 'deliveryservice_stats'")
+               c.DSDBName = "deliveryservice_stats"
+       }
+
+       if c.CacheDBName == "" {
+               log.Warnln("InfluxDB configuration does not specify a Cache 
Stats DB name - falling back on 'cache_stats'")
+               c.CacheDBName = "cache_stats"
+       }
+
+       if c.Secure == nil {
+               log.Warnln("InfluxDB configuration does not specify 'secure', 
defaulting to 'true'")
+               c.Secure = util.BoolPtr(true)
+       }
+
+       return true, &c, nil
+}
+
 func getLDAPConf(s string) (*ConfigLDAP, error) {
        ldapConf := ConfigLDAP{LDAPTimeoutSecs: DefaultLDAPTimeoutSecs} //if 
the field is not set in the config we use the default instead of 0
        err := json.Unmarshal([]byte(s), &ldapConf)
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go 
b/traffic_ops/traffic_ops_golang/routing/routes.go
index 28a47ae..2ec9a5b 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -75,6 +75,7 @@ import (
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/steering"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/steeringtargets"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/systeminfo"
+       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficstats"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/types"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/urisigning"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/user"
@@ -111,6 +112,9 @@ func Routes(d ServerData) ([]Route, []RawRoute, 
http.Handler, error) {
                {1.1, http.MethodPost, `asns/?$`, 
api.CreateHandler(&asn.TOASNV11{}), auth.PrivLevelOperations, Authenticated, 
nil},
                {1.1, http.MethodDelete, `asns/{id}$`, 
api.DeleteHandler(&asn.TOASNV11{}), auth.PrivLevelOperations, Authenticated, 
nil},
 
+               // Traffic Stats access
+               {1.2, http.MethodGet, `deliveryservice_stats`, 
trafficstats.GetDSStats, auth.PrivLevelOperations, Authenticated, nil},
+
                {1.1, http.MethodGet, `caches/stats/?(\.json)?$`, 
cachesstats.Get, auth.PrivLevelReadOnly, Authenticated, nil},
 
                //CacheGroup: CRUD
diff --git a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go 
b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
index 7b1f834..6392c42 100644
--- a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
+++ b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
@@ -74,10 +74,10 @@ func main() {
        }
 
        cfg, errsToLog, blockStart := config.LoadConfig(*configFileName, 
*dbConfigFileName, *riakConfigFileName, version)
+       for _, err := range errsToLog {
+               log.Errorln(err)
+       }
        if blockStart {
-               for _, err := range errsToLog {
-                       fmt.Println(err)
-               }
                os.Exit(1)
        }
 
diff --git a/traffic_ops/traffic_ops_golang/trafficstats/trafficstats.go 
b/traffic_ops/traffic_ops_golang/trafficstats/trafficstats.go
new file mode 100644
index 0000000..b1b2fb4
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/trafficstats/trafficstats.go
@@ -0,0 +1,595 @@
+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"
+import "encoding/json"
+import "errors"
+import "fmt"
+import "net/http"
+import "strconv"
+import "strings"
+import "time"
+
+import "github.com/apache/trafficcontrol/lib/go-tc"
+import "github.com/apache/trafficcontrol/lib/go-rfc"
+import "github.com/apache/trafficcontrol/lib/go-log"
+import "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+import "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
+
+import influx "github.com/influxdata/influxdb1-client/v2"
+
+var (
+       metricTypes = map[string]interface{}{
+               "kbps":      struct{}{},
+               "tps_total": struct{}{},
+               "tps_2xx":   struct{}{},
+               "tps_3xx":   struct{}{},
+               "tps_4xx":   struct{}{},
+               "tps_5xx":   struct{}{},
+       }
+
+       jsonWithRFCTimestamps = rfc.MimeType{
+               "application/json",
+               map[string]string{"timestamp": "rfc"},
+       }
+
+       jsonWithUnixTimestamps = rfc.MimeType{
+               "application/json",
+               map[string]string{"timestamp": "unix"},
+       }
+)
+
+const (
+       DEFAULT_INTERVAL         = "1m"
+       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.
+       summaryQuery = `
+               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`
+
+       seriesQuery = `
+               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 ConfigFromRequest(r *http.Request, i *api.APIInfo) 
(tc.TrafficStatsConfig, error, int) {
+       var c tc.TrafficStatsConfig
+       var e error
+       if accept := r.Header.Get("Accept"); accept != "" {
+
+               mimes, err := rfc.MimeTypesFromAccept(accept)
+               if err != nil {
+                       log.Warnf("Failed to negotiate content, Accept line 
'%s', error: %v", accept, err)
+               } else {
+
+                       found := false
+                       for _, m := range mimes {
+                               if jsonWithRFCTimestamps.Satisfy(m) {
+                                       found = true
+                                       break
+                               }
+
+                               if jsonWithUnixTimestamps.Satisfy(m) {
+                                       found = true
+                                       c.Unix = true
+                                       break
+                               }
+                       }
+
+                       if !found {
+                               e = fmt.Errorf("Failed to negotiate content; 
cannot produce output satisfying %s", accept)
+                               return c, e, http.StatusNotAcceptable
+                       }
+               }
+       }
+
+       if limit, ok := i.Params["limit"]; ok {
+               lim, err := strconv.ParseUint(limit, 10, 64)
+               if err != nil {
+                       e = errors.New("Invalid limit!")
+                       return c, e, http.StatusBadRequest
+               }
+               c.Limit = &lim
+       }
+
+       if offset, ok := i.Params["offset"]; ok {
+               off, err := strconv.ParseUint(offset, 10, 64)
+               if err != nil {
+                       e = errors.New("Invalid offset!")
+                       return c, e, http.StatusBadRequest
+               }
+               c.Offset = &off
+       }
+
+       if orderby, ok := i.Params["orderby"]; ok {
+               if c.OrderBy = tc.OrderableFromString(orderby); c.OrderBy == 
nil {
+                       e = errors.New("Invalid orderby! Must be 'time' or 
'value'")
+                       return c, e, http.StatusBadRequest
+               }
+       }
+
+       if c.Start, e = parseTime(i.Params["startDate"]); e != nil {
+               log.Errorf("Parsing startDate: %v", e)
+               e = errors.New("Invalid startDate!")
+               return c, e, http.StatusBadRequest
+       }
+
+       if c.End, e = parseTime(i.Params["endDate"]); e != nil {
+               log.Errorf("Parsing endDate: %v", e)
+               e = errors.New("Invalid endDate!")
+               return c, e, http.StatusBadRequest
+       }
+
+       if interval, ok := i.Params["interval"]; !ok {
+               c.Interval = DEFAULT_INTERVAL
+       } else if !tc.TrafficStatsDurationPattern.MatchString(interval) {
+               e = errors.New("interval: must be a valid InfluxQL duration 
literal (resolution no less than minute)")
+               return c, e, http.StatusBadRequest
+       } else {
+               c.Interval = interval
+       }
+
+       if ex, ok := i.Params["exclude"]; ok {
+               switch tc.ExcludeFromString(ex) {
+               case tc.ExcludeSummary:
+                       c.ExcludeSummary = true
+               case tc.ExcludeSeries:
+                       c.ExcludeSeries = true
+               default:
+                       e = errors.New("Invalid exclude! Must be 'series' or 
'summary'")
+                       return c, e, http.StatusBadRequest
+               }
+       }
+
+       c.MetricType = i.Params["metricType"]
+       if _, ok := metricTypes[c.MetricType]; !ok {
+               e = fmt.Errorf("Unknown metric type: %s", c.MetricType)
+               return c, e, http.StatusBadRequest
+       }
+
+       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, e, http.StatusBadRequest
+               }
+
+               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, e, http.StatusInternalServerError
+                       } else if !exists {
+                               c.DeliveryService = xmlid
+                       }
+               }
+       }
+
+       return c, nil, http.StatusOK
+}
+
+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.TrafficStatsConfig
+       if c, userErr, errCode = ConfigFromRequest(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
+       }
+
+       resp := struct{ Response tc.TrafficStatsResponse `json:"response"` }{
+               Response: tc.TrafficStatsResponse{
+                       Source:  tc.TRAFFIC_STATS_SOURCE,
+                       Version: tc.TRAFFIC_STATS_VERSION,
+                       Series:  nil,
+                       Summary: nil,
+               },
+       }
+
+       // 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.
+       if !c.ExcludeSummary {
+               summary, messages, err := getSummary(client, &c, 
inf.Config.ConfigInflux.DSDBName)
+               log.Debugf("Messages from summary query: %s", 
tc.MessagesToString(messages))
+
+               if err != nil {
+                       sysErr = fmt.Errorf("Getting summary response from 
Influx: %v", err)
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
nil, sysErr)
+                       return
+               }
+
+               resp.Response.Summary = &summary
+       }
+
+       if !c.ExcludeSeries {
+               series, messages, err := getSeries(client, &c, 
inf.Config.ConfigInflux.DSDBName)
+               log.Debugf("Messages from series query: %s", 
tc.MessagesToString(messages))
+
+               if err != nil {
+                       sysErr = fmt.Errorf("Getting summary response from 
Influx: %v", err)
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
nil, sysErr)
+                       return
+               }
+
+               if !c.Unix {
+                       series.FormatTimestamps()
+               }
+
+               resp.Response.Series = &series
+       }
+
+       respBts, err := json.Marshal(resp)
+       if err != nil {
+               sysErr = fmt.Errorf("Marshalling response: %v", err)
+               errCode = http.StatusInternalServerError
+               api.HandleErr(w, r, tx, errCode, nil, sysErr)
+               return
+       }
+
+       if c.Unix {
+               w.Header().Set(tc.ContentType, jsonWithUnixTimestamps.String())
+       } else {
+               w.Header().Set(tc.ContentType, jsonWithRFCTimestamps.String())
+       }
+       w.Header().Set(http.CanonicalHeaderKey("vary"), 
http.CanonicalHeaderKey("Accept"))
+       w.Write(append(respBts, '\n'))
+}
+
+func getSummary(client *influx.Client, conf *tc.TrafficStatsConfig, db string) 
(tc.TrafficStatsSummary, []influx.Message, error) {
+
+       msgs := []influx.Message{}
+       s := tc.TrafficStatsSummary{}
+       qStr := fmt.Sprintf(summaryQuery, 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),
+               })
+       q.RetentionPolicy = "monthly"
+
+       log.Debugf("InfluxDB SummaryQuery: %+v", q)
+
+       resp, err := (*client).Query(q)
+       if err != nil {
+               return s, msgs, err
+       }
+
+       if resp.Results != nil && len(resp.Results) == 1 {
+               r := resp.Results[0]
+               if r.Messages != nil {
+                       for _, m := range r.Messages {
+                               if m != nil {
+                                       msgs = append(msgs, *m)
+                               }
+                       }
+               }
+
+               if len(r.Series) != 1 {
+                       return s, msgs, fmt.Errorf("Improper number of series: 
%d", len(r.Series))
+               }
+
+               series := r.Series[0]
+
+               if len(series.Values) != 1 {
+                       return s, msgs, fmt.Errorf("Improper number of returned 
rows: %d", len(r.Series[0].Values))
+               }
+
+               vals := series.Values[0]
+               if len(vals) != 8 || len(series.Columns) != 8 {
+                       return s, msgs, fmt.Errorf("Improper number of returned 
values in row: %d (%d cols)", len(vals), len(series.Columns))
+               }
+
+               mappedValues := map[string]interface{}{}
+               for i, v := range vals {
+                       mappedValues[series.Columns[i]] = v
+               }
+
+               var err error
+               if s.Average, err = extractFloat64("average", mappedValues); 
err != nil {
+                       return s, msgs, err
+               }
+
+               if s.Count, err = extractUInt("count", mappedValues); err != 
nil {
+                       return s, msgs, err
+               }
+
+               if s.FifthPercentile, err = extractFloat64("fifthPercentile", 
mappedValues); err != nil {
+                       return s, msgs, err
+               }
+
+               if s.Max, err = extractFloat64("max", mappedValues); err != nil 
{
+                       return s, msgs, err
+               }
+
+               if s.Min, err = extractFloat64("min", mappedValues); err != nil 
{
+                       return s, msgs, err
+               }
+
+               if s.Max, err = extractFloat64("max", mappedValues); err != nil 
{
+                       return s, msgs, err
+               }
+
+               if s.NinetyEighthPercentile, err = 
extractFloat64("ninetyEighthPercentile", mappedValues); err != nil {
+                       return s, msgs, err
+               }
+
+               if s.NinetyFifthPercentile, err = 
extractFloat64("ninetyFifthPercentile", mappedValues); err != nil {
+                       return s, msgs, err
+               }
+
+       } else {
+               log.Debugf("InfluxDB summary response: %+v", resp)
+               return s, msgs, errors.New("'results' missing or improper!")
+       }
+
+       if resp.Error() != nil {
+               log.Debugf("response error, summary object was: %+v", s)
+               return s, msgs, resp.Error()
+       }
+
+       value := float64(s.Count*60) * s.Average
+       if conf.MetricType == "kbps" {
+               value /= 1000
+               s.TotalBytes = &value
+       } else {
+               s.TotalTransactions = &value
+       }
+
+       return s, msgs, nil
+}
+
+func extractUInt(k string, m map[string]interface{}) (uint, error) {
+       tmp, ok := m[k]
+       if !ok {
+               return 0, fmt.Errorf("response has no value for column %s", k)
+       }
+       switch t := tmp.(type) {
+       case float64:
+               return uint(tmp.(float64)), nil
+       case json.Number:
+               ret, err := tmp.(json.Number).Int64()
+               if err != nil {
+                       return 0, fmt.Errorf("Error parsing value for column 
'%s' as an int64: %v", k, err)
+               }
+               return uint(ret), nil
+       default:
+               return 0, fmt.Errorf("invalid type for column '%s' - expected 
unsigned integer, got %T (%v)", k, t, tmp)
+       }
+}
+
+func extractFloat64(k string, m map[string]interface{}) (float64, error) {
+       tmp, ok := m[k]
+       if !ok {
+               return 0, fmt.Errorf("response has no value for column %s", k)
+       }
+
+       switch t := tmp.(type) {
+       case float64:
+               return tmp.(float64), nil
+       case json.Number:
+               ret, err := tmp.(json.Number).Float64()
+               if err != nil {
+                       return 0, fmt.Errorf("Error parsing value for column 
'%s' as a float64: %v", k, err)
+               }
+               return ret, nil
+       case nil:
+               // This is the only field that can be nil - because sometimes 
there isn't enough data for
+               // 5% of it to be below a value. Could probably coalesce this 
in the query at some point, though.
+               if k == "fifthPercentile" {
+                       return 0, nil
+               }
+               return 0, fmt.Errorf("column '%s' was null/blank", k)
+       default:
+               return 0, fmt.Errorf("invalid type for column '%s' - expected 
'float64' or 'json.Number', got %T (%v)", k, t, tmp)
+       }
+}
+
+func parseTime(raw string) (time.Time, error) {
+       t, e := time.Parse(time.RFC3339Nano, raw)
+       if e == nil {
+               return t, nil
+       }
+
+       if i, err := strconv.ParseInt(raw, 10, 64); err == nil {
+               t = time.Unix(0, i)
+               return t, nil
+       }
+
+       return time.Parse(tc.TimeLayout, raw)
+}
+
+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 getSeries(client *influx.Client, conf *tc.TrafficStatsConfig, db string) 
(tc.TrafficStatsSeries, []influx.Message, error) {
+       s := tc.TrafficStatsSeries{}
+       msgs := []influx.Message{}
+       extraClauses := strings.Builder{}
+       if conf.OrderBy != nil {
+               extraClauses.Write([]byte(" ORDER BY "))
+               extraClauses.WriteString(string(*conf.OrderBy))
+       }
+
+       if conf.Limit != nil {
+               extraClauses.WriteString(fmt.Sprintf(" LIMIT %d", *conf.Limit))
+       }
+
+       if conf.Offset != nil {
+               extraClauses.WriteString(fmt.Sprintf(" OFFSET %d", 
*conf.Offset))
+       }
+
+       qStr := fmt.Sprintf(seriesQuery, db, conf.MetricType, conf.Interval, 
conf.OffsetString(), extraClauses.String())
+       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,
+               })
+       q.RetentionPolicy = "monthly"
+
+       log.Debugf("InfluxDB series query: %+v", q)
+
+       resp, err := (*client).Query(q)
+       if err != nil {
+               return s, msgs, err
+       }
+
+       if resp.Results != nil && len(resp.Results) == 1 {
+               r := resp.Results[0]
+               if r.Messages != nil {
+                       for _, m := range r.Messages {
+                               if m != nil {
+                                       msgs = append(msgs, *m)
+                               }
+                       }
+               }
+
+               if len(r.Series) != 1 {
+                       return s, msgs, fmt.Errorf("Improper number of series: 
%d", len(r.Series))
+               }
+
+               series := r.Series[0]
+
+               s = tc.TrafficStatsSeries{
+                       Name:    series.Name,
+                       Tags:    series.Tags,
+                       Values:  series.Values,
+                       Columns: series.Columns,
+                       Count:   uint(len(series.Values)),
+               }
+
+       } else {
+               log.Debugf("InfluxDB series response: %+v", resp)
+               return s, msgs, errors.New("'results' missing or improper!")
+       }
+
+       if resp.Error() != nil {
+               log.Debugf("response error, series object was %+v", s)
+               return s, msgs, resp.Error()
+       }
+
+       return s, msgs, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/trafficstats/trafficstats_test.go 
b/traffic_ops/traffic_ops_golang/trafficstats/trafficstats_test.go
new file mode 100644
index 0000000..e96a629
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/trafficstats/trafficstats_test.go
@@ -0,0 +1,106 @@
+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 "net/http"
+import "testing"
+import "time"
+
+import "github.com/apache/trafficcontrol/lib/go-tc"
+import "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+
+func TestConfigFromRequest(t *testing.T) {
+       start := "2019-09-30T00:00:00Z"
+       startTime, err := time.Parse(time.RFC3339, start)
+       if err != nil {
+               t.Fatalf("Failed to parse test start time: %v", err)
+       }
+       end := "2019-10-01 00:00:00-07"
+       endTime, err := time.Parse(tc.TimeLayout, end)
+       if err != nil {
+               t.Fatalf("Failed to parse test end time: %v", err)
+       }
+
+       inf := api.APIInfo{
+               Params: map[string]string{
+                       "limit":           "10",
+                       "offset":          "0",
+                       "orderby":         "time",
+                       "startDate":       start,
+                       "endDate":         end,
+                       "interval":        "1m",
+                       "metricType":      "tps_total",
+                       "deliveryService": "test",
+               },
+       }
+
+       r, e := http.NewRequest(http.MethodGet, 
"https://example.test/api/1.4/deliveryservice_stats";, nil)
+       if e != nil {
+               t.Fatalf("Failed to build test request: %v", e)
+       }
+       r.Header.Add(tc.ContentType, tc.ApplicationJson)
+
+       cfg, err, code := ConfigFromRequest(r, &inf)
+       if err != nil {
+               t.Errorf("Unexpected error: %v", err)
+       }
+       if code != http.StatusOK {
+               t.Errorf("Expected OK status, but was %d", code)
+       }
+       if cfg.DeliveryService != "test" {
+               t.Errorf("Expected config to have DS 'test', but was '%s'", 
cfg.DeliveryService)
+       }
+       if !cfg.End.Equal(endTime) {
+               t.Errorf("Expected end time to be %v, but was %v", endTime, 
cfg.End)
+       }
+       if !cfg.Start.Equal(startTime) {
+               t.Errorf("Expected start time to be %v, but was %v", startTime, 
cfg.Start)
+       }
+       if cfg.ExcludeSeries {
+               t.Errorf("Expected series to not be excluded, but it was")
+       }
+       if cfg.ExcludeSummary {
+               t.Errorf("Expected summary to not be excluded, but it was")
+       }
+       if cfg.Interval != tc.OneMinute {
+               t.Errorf("Expected interval to be '1m', but it was %s", 
cfg.Interval)
+       }
+       if cfg.Limit == nil {
+               t.Errorf("Expected limit to not be nil, but it was")
+       } else if *cfg.Limit != 10 {
+               t.Errorf("Expected limit to be 10, but it was %d", *cfg.Limit)
+       }
+       if cfg.MetricType != "tps_total" {
+               t.Errorf("Expected metric type to be tps_total, but it was %s", 
cfg.MetricType)
+       }
+       if cfg.Offset == nil {
+               t.Errorf("Expected offset to not be nil, but it was")
+       } else if *cfg.Offset != 0 {
+               t.Errorf("Expected offset to be 0, but it was %d", *cfg.Offset)
+       }
+       if cfg.OrderBy == nil {
+               t.Errorf("Expected Order By to not be nil, but it was")
+       } else if *cfg.OrderBy != tc.TimeOrder {
+               t.Errorf("Expected Order by to be time, but it was %s", 
*cfg.OrderBy)
+       }
+       if cfg.Unix {
+               t.Errorf("Expected Unix to not be set without MIME parameter, 
but it was")
+       }
+}
diff --git a/traffic_portal/app/src/common/api/DeliveryServiceStatsService.js 
b/traffic_portal/app/src/common/api/DeliveryServiceStatsService.js
index ea97fbe..1898c3d 100644
--- a/traffic_portal/app/src/common/api/DeliveryServiceStatsService.js
+++ b/traffic_portal/app/src/common/api/DeliveryServiceStatsService.js
@@ -23,7 +23,7 @@ var DeliveryServiceStatsService = function($http, $q, ENV, 
messageModel) {
                var request = $q.defer();
 
                var url = ENV.api['root'] + "deliveryservice_stats",
-                       params = { deliveryServiceName: xmlId, metricType: 
'kbps', serverType: 'edge', startDate: start.seconds(00).format(), endDate: 
end.seconds(00).format(), interval: '60s' };
+                       params = { deliveryServiceName: xmlId, metricType: 
'kbps', serverType: 'edge', startDate: start.seconds(00).format(), endDate: 
end.seconds(00).format(), interval: '1m' };
 
                $http.get(url, { params: params })
                        .then(
@@ -43,7 +43,7 @@ var DeliveryServiceStatsService = function($http, $q, ENV, 
messageModel) {
                var request = $q.defer();
 
                var url = ENV.api['root'] + "deliveryservice_stats",
-                       params = { deliveryServiceName: xmlId, metricType: 
'tps_total', serverType: 'edge', startDate: start.seconds(00).format(), 
endDate: end.seconds(00).format(), interval: '60s' };
+                       params = { deliveryServiceName: xmlId, metricType: 
'tps_total', serverType: 'edge', startDate: start.seconds(00).format(), 
endDate: end.seconds(00).format(), interval: '1m' };
 
                $http.get(url, { params: params })
                        .then(
@@ -63,7 +63,7 @@ var DeliveryServiceStatsService = function($http, $q, ENV, 
messageModel) {
                var request = $q.defer();
 
                var url = ENV.api['root'] + "deliveryservice_stats",
-                       params = { deliveryServiceName: xmlId, metricType: 
'tps_' + httpStatus, serverType: 'edge', startDate: start.seconds(00).format(), 
endDate: end.seconds(00).format(), interval: '60s' };
+                       params = { deliveryServiceName: xmlId, metricType: 
'tps_' + httpStatus, serverType: 'edge', startDate: start.seconds(00).format(), 
endDate: end.seconds(00).format(), interval: '1m' };
 
                $http.get(url, { params: params })
                        .then(

Reply via email to