+ added sample logging mechanisms for StatsD in api-gateway-config, which for now it's only activated when the container is deployed in Marathon
diff --git a/Dockerfile b/Dockerfile
index 9a160fa..0ae5ea4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,7 +8,7 @@
RUN apk update \
&& apk add gcc tar libtool zlib jemalloc jemalloc-dev perl \
- make musl-dev openssl-dev pcre-dev g++ zlib-dev curl
+ make musl-dev openssl-dev pcre-dev g++ zlib-dev curl python
ENV OPENRESTY_VERSION 1.9.3.1
ENV NAXSI_VERSION 0.53-2
diff --git a/api-gateway-config/api-gateway-reload-config.sh b/api-gateway-config/api-gateway-reload-config.sh
deleted file mode 100755
index bbe7a5a..0000000
--- a/api-gateway-config/api-gateway-reload-config.sh
+++ /dev/null
@@ -1,63 +0,0 @@
-#/*
-# * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
-# *
-# * Permission is hereby granted, free of charge, to any person obtaining a
-# * copy of this software and associated documentation files (the "Software"),
-# * to deal in the Software without restriction, including without limitation
-# * the rights to use, copy, modify, merge, publish, distribute, sublicense,
-# * and/or sell copies of the Software, and to permit persons to whom the
-# * Software is furnished to do so, subject to the following conditions:
-# *
-# * The above copyright notice and this permission notice shall be included in
-# * all copies or substantial portions of the Software.
-# *
-# * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-# * DEALINGS IN THE SOFTWARE.
-# *
-# */
-#!/bin/sh
-
-# this script checks to see if there's any modification to the configuration files since the last run
-# to automatically reload the configuration.
-
-# NOTE: not used for now
-
-# The script may be executed as part of a cronjob running every minute like
-# * * * * * /etc/api-gateway/api-gateway-reload-config.sh 1>&2 > /var/log/api-gateway/reload-config-cron.log
-
-LOG_DIR=/var/log/api-gateway
-LOG_FILE=$LOG_DIR/reload-config.log
-
-function do_log()
-{
- local _MSG=$1
- echo "[`date +'%Y-%m-%d-%H:%M:%S'`] - ${_MSG}"
-}
-
-function fatal_error()
-{
- local _MSG=$1
- do_log "ERROR: ${_MSG}"
- exit 255
-}
-
-function info_log()
-{
- local _MSG=$1
- do_log "${_MSG}"
-}
-
-
-changed_files=$(find /etc/api-gateway -type f -newer /var/run/apigateway-config-watcher.lastrun -print)
-if [[ -n "${changed_files}" ]]; then
- info_log "discovered changed files ..."
- info_log ${changed_files}
- info_log "reloading gateway ..."
- api-gateway -t -p /usr/local/api-gateway/ -c /etc/api-gateway/api-gateway.conf && api-gateway -s reload
-fi
-echo `date` > /var/run/apigateway-config-watcher.lastrun
\ No newline at end of file
diff --git a/api-gateway-config/conf.d/api_gateway_init.conf b/api-gateway-config/conf.d/api_gateway_init.conf
index e21699e..1c206e5 100644
--- a/api-gateway-config/conf.d/api_gateway_init.conf
+++ b/api-gateway-config/conf.d/api_gateway_init.conf
@@ -47,10 +47,15 @@
# be prepared to load any custom lua scripts from /etc/api-gateway/scripts
-lua_package_path '/etc/api-gateway/scripts/?.lua;;';
-init_worker_by_lua_file /etc/api-gateway/scripts/api_gateway_init.lua;
+lua_package_path '/etc/api-gateway/scripts/lua/?.lua;;';
+init_worker_by_lua_file /etc/api-gateway/scripts/lua/api_gateway_init.lua;
lua_shared_dict cachedkeys 50m; # caches api-keys
lua_shared_dict cachedOauthTokens 50m; # caches OAuth tokens
lua_shared_dict cachedUserProfiles 50m; # caches user profiles
-lua_shared_dict healthcheck_redis 1m; # used by lua health_check for redis cache
\ No newline at end of file
+lua_shared_dict healthcheck_redis 1m; # used by lua health_check for redis cache
+
+# metrics
+lua_shared_dict stats_counters 50m;
+lua_shared_dict stats_timers 50m;
+lua_shared_dict stats_all 50m;
\ No newline at end of file
diff --git a/api-gateway-config/conf.d/includes/analytics_endpoints.conf b/api-gateway-config/conf.d/includes/analytics_endpoints.conf
index 2c4ea0c..3dbca5e 100644
--- a/api-gateway-config/conf.d/includes/analytics_endpoints.conf
+++ b/api-gateway-config/conf.d/includes/analytics_endpoints.conf
@@ -27,7 +27,7 @@
allow 127.0.0.1;
deny all;
content_by_lua '
- local MetricsCls = require "legacy.metrics"
+ local MetricsCls = require "metrics.MetricsBuffer"
local metrics = MetricsCls:new()
local json = assert( metrics:toJson(), "Could not read metrics")
ngx.say( json )
diff --git a/api-gateway-config/marathon-service-discovery.sh b/api-gateway-config/marathon-service-discovery.sh
index 03caa2e..dd0d265 100755
--- a/api-gateway-config/marathon-service-discovery.sh
+++ b/api-gateway-config/marathon-service-discovery.sh
@@ -1,3 +1,4 @@
+#!/bin/sh
#/*
# * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
# *
@@ -20,7 +21,6 @@
# * DEALINGS IN THE SOFTWARE.
# *
# */
-#!/bin/sh
#
# Overview:
@@ -54,6 +54,9 @@
# NOTE: for the moment when tasks expose multiple ports, only the first one is exposed through nginx
curl -s ${marathon_host}/v2/tasks -H "Accept:text/plain" | awk 'NF>2' | grep -v :0 | awk '!seen[$1]++' | awk ' {s=""; for (f=3; f<=NF; f++) s = s "\n server " $f " fail_timeout=10s;" ; print "upstream " $1 " {" s "\n keepalive 16;\n}" }' > ${TMP_FILE}
# 1.1. check redis upstreams
+#
+# ASSUMPTION: there is a redis app named "api-gateway-redis" deployed in marathon and optionally another app named "api-gateway-redis-replica"
+#
redis_master=$(cat ${TMP_FILE} | grep api-gateway-redis | wc -l)
redis_replica=$(cat ${TMP_FILE} | grep api-gateway-redis-replica | wc -l)
# if api-gateway-redis upstream exists but api-gateway-redis-replica does not, then create the replica
diff --git a/api-gateway-config/scripts/api_gateway_init.lua b/api-gateway-config/scripts/lua/api_gateway_init.lua
similarity index 93%
rename from api-gateway-config/scripts/api_gateway_init.lua
rename to api-gateway-config/scripts/lua/api_gateway_init.lua
index b511058..db5a42c 100644
--- a/api-gateway-config/scripts/api_gateway_init.lua
+++ b/api-gateway-config/scripts/lua/api_gateway_init.lua
@@ -48,7 +48,12 @@
parentObject.validation = require "api-gateway.validation.factory"
end
+local function initMetricsFactory(parentObject)
+ parentObject.metrics = require "metrics.factory"
+end
+
initValidationFactory(_M)
+initMetricsFactory(_M)
-- TODO: test health-check with the new version of Openresty
-- initRedisHealthCheck()
diff --git a/api-gateway-config/scripts/lua/metrics/MetricsBuffer.lua b/api-gateway-config/scripts/lua/metrics/MetricsBuffer.lua
new file mode 100644
index 0000000..47d322b
--- /dev/null
+++ b/api-gateway-config/scripts/lua/metrics/MetricsBuffer.lua
@@ -0,0 +1,165 @@
+-- Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+-- Base class for analytics with functions for calculating counts,timers
+-- and updating the cache with data on every service call.
+--
+--
+--
+-- Usage in Nginx conf:
+--
+-- location api-gateway-stats {
+-- allow 127.0.0.1;
+-- deny all;
+-- content_by_lua '
+-- local MetricsBuffer = require "metrics.MetricsBuffer"
+-- local metrics = MetricsBuffer:new()
+-- local json = assert( metrics:toJson(), "Could not read metrics")
+-- ngx.say( json )
+-- ';
+-- }
+
+local cjson = require "cjson"
+
+local M = {}
+
+function M:new(o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ return o
+end
+
+--- Increments a counter with the given <value>
+-- @param metric The name of the metrics
+-- @param value The value to intecrement with
+--
+function M:count(metric, value)
+ local localMetrics = ngx.shared.stats_counters
+ local r
+ if ( localMetrics == nil ) then
+ ngx.log(ngx.ERROR, "Please define 'lua_shared_dict stats_counters 50m;' in http block")
+ return nil
+ end
+
+ value = tonumber(value) or 0
+ if value == 0 then -- to exit when nil/zero
+ return 0
+ end
+
+ r = localMetrics:incr(metric, value)
+ if ( r == nil ) then
+ localMetrics:add(metric, value)
+ return value
+ end
+ return r
+end
+
+--- Adds a new timer value. Unline counters, timers are averaged when flushed
+-- @param metric The name of the metrics
+-- @param value A new value for the timer
+--
+function M:timer(metric, value)
+ local localMetrics = ngx.shared.stats_timers
+ local r, counter
+ if ( localMetrics == nil ) then
+ ngx.log(ngx.ERROR, "Please define 'lua_shared_dict stats_timers 50m;' in http block")
+ return nil
+ end
+
+ value = tonumber(value) or -2
+
+ -- Timers are counting only the positive values, by convention
+ if ( value < 0 ) then
+ return value
+ end
+
+ r, counter = localMetrics:get(metric)
+ if ( r == nil ) then
+ localMetrics:set(metric, value, 0, 1)
+ return value
+ end
+ -- FIXES: attempt to perform arithmetic on local 'counter' (a nil value)
+ if ( counter == nil ) then
+ counter = 0
+ end
+ -- Adding the timers and increamenting the counter to compute avg later
+ counter = counter + 1
+ r = r + value
+ localMetrics:set(metric, r, 0, counter)
+ return r
+end
+
+function M:getJsonFor( metric_type )
+ -- convert shared_dict to table
+ local localMetrics = ngx.shared[metric_type]
+ if ( localMetrics == nil ) then
+ return nil
+ end
+ local keys = localMetrics:get_keys(1024)
+ local counter
+ local jsonObj = {}
+ local count = 0
+ for i,metric in pairs(keys) do
+ local value, counter = localMetrics:get(metric)
+ if(counter == nil) then
+ counter = 0
+ end
+ -- check if avg needs to be computed
+ if(counter == 1 or counter == 0) then
+ jsonObj[metric] = value
+ end
+ if(counter >= 2) then
+ jsonObj[metric] = math.floor(value/counter)
+ end
+ -- mark item as expired
+ localMetrics:set(metric, 0, 0.001,0)
+ count = i
+ end
+ return cjson.encode(jsonObj),count,jsonObj
+end
+
+function M:toJson( flushExpiredMetrics )
+ local counters, count_counters, counterObject = self:getJsonFor("stats_counters")
+ local timers, count_timers, timerObject = self:getJsonFor("stats_timers")
+ local flush = flushExpiredMetrics or true
+
+ -- ngx.log(ngx.INFO, "Wrote " .. count_counters .. " counters and " .. count_timers .. " timers.")
+
+ if ( flush == true ) then
+ self:flushExpiredKeys()
+ end
+ return "{\"counters\":" .. counters .. ",\"timers\":" .. timers .. "}", counterObject, timerObject
+end
+
+function M:flushExpiredKeys()
+ local metrics = ngx.shared.stats_counters
+ if ( metrics ~= nil ) then
+ metrics:flush_expired()
+ end
+
+ metrics = ngx.shared.stats_timers
+ if ( metrics ~= nil ) then
+ metrics:flush_expired()
+ end
+end
+
+return M
+
diff --git a/api-gateway-config/scripts/lua/metrics/MetricsCollector.lua b/api-gateway-config/scripts/lua/metrics/MetricsCollector.lua
new file mode 100644
index 0000000..624fb1f
--- /dev/null
+++ b/api-gateway-config/scripts/lua/metrics/MetricsCollector.lua
@@ -0,0 +1,185 @@
+-- Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+-- Records the metrics for the current request.
+--
+-- # Sample StatsD messages
+-- # pub.publisher_name.consumer.consumer_name.app.application_name.service.service_name.prod.region.useast.request.device_links.POST.200.count
+-- # pub.publisher_name.consumer.consumer_name.app.application_name.service.service_name.prod.region.useast.request.device_links.POST.200.responseTime
+-- # pub.publisher_name.consumer.consumer_name.app.application_name.service.service_name.prod.region.useast.request.device_links.POST.200.upstreamResponseTime
+-- # pub.publisher_name.consumer.consumer_name.app.application_name.service.service_name.prod.region.useast.request.validate_request.GET.200.responseTime
+--
+-- NOTE: by default it logs the root-path of the request. If the root path is used for versioning ( i.e. v1.0 ) there is the property $metric_path that
+-- can be set on the location block in order to override the root-path
+--
+-- Created by IntelliJ IDEA.
+-- User: nramaswa
+-- Date: 3/14/14
+-- Time: 12:45 AM
+-- To change this template use File | Settings | File Templates.
+--
+local MetricsCls = require "metrics.MetricsBuffer"
+local metrics = MetricsCls:new()
+
+local M = {}
+
+function M:new(o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ return o
+end
+
+-- To replace . with _
+local function normalizeString(input)
+ return input:gsub("%.", "_")
+end
+
+local function getPathForMetrics(serviceName)
+ local user_defined_path = ngx.var.metrics_path
+ if user_defined_path ~= nil and #user_defined_path < 2 then
+ user_defined_path = nil
+ end
+
+ local request_uri = user_defined_path or ngx.var.uri or "/" .. serviceName
+ local pattern = "(?:\\/?\\/?)([^\\/:]+)" -- To get the first part of the request uri excluding content after :
+ local requestPathFromURI, err = ngx.re.match(request_uri, pattern)
+ local requestPath = serviceName -- default value
+
+ if err then
+ ngx.log(ngx.WARN, "Assigned requestPath as serviceName due to error in extracting requestPathFromURI: ", err)
+ end
+
+ if requestPathFromURI then
+ if requestPathFromURI[1] then
+ requestPath = requestPathFromURI[1]
+ requestPath = requestPath:gsub("%.", "_")
+ --ngx.log(ngx.INFO, "\n the extracted requestPath::"..requestPath)
+ end
+ end
+ return requestPath
+end
+
+function M:logCurrentRequest()
+ -- Variables used in the bucket path
+ local publisherName = ngx.var.publisher_org_name or "undefined"
+ local consumerName = ngx.var.consumer_org_name or "undefined"
+ local appName = ngx.var.app_name or "undefined"
+ local serviceName = ngx.var.service_name or "undefined"
+ local realm = ngx.var.service_env or ngx.var.realm or "sandbox"
+ local requestMethod = ngx.var.request_method or "undefined"
+ local status = ngx.var.status or "0"
+ local validateRequestStatus = ngx.var.validate_request_status or "0"
+
+ -- Values for metrics - converted tonumber() later
+ local bucketCountValue = 1
+ local requestTime = tonumber(ngx.var.request_time) or -1
+ local upstreamResponseTime = tonumber(ngx.var.upstream_response_time) or -1
+ local validateTime = tonumber(ngx.var.validate_request_response_time) or -1
+ local rgnName = ngx.var.aws_region or "undefined"
+ local bytesSent = tonumber(ngx.var.bytes_sent) or -1
+ local bytesReceived = tonumber(ngx.var.request_length) or -1
+
+ -- ---------------------------------------------------------------------- --
+ -- ------------- Logging for all the requests --------------- --
+ -- ---------------------------------------------------------------------- --
+
+ local bucket = "publisher." .. normalizeString(publisherName) ..
+ ".consumer." .. normalizeString(consumerName) ..
+ ".application." .. normalizeString(appName) ..
+ ".service." .. normalizeString(serviceName) ..
+ "." .. realm ..
+ ".region." .. rgnName ..
+ ".request.";
+ local validate_request_response_time = bucket .. "validate_request." .. validateRequestStatus .. ".responseTime";
+
+ local requestPath = getPathForMetrics(serviceName)
+
+ local bytes_sent = bucket .. "bytesSent";
+ local bytes_received = bucket .. "bytesReceived";
+
+
+ --bandwidth data - update only if its greater than zero to sum up all calls
+ if bytesSent > 0 then
+ metrics:count(bytes_sent, bytesSent)
+ end
+ if bytesReceived > 0 then
+ metrics:count(bytes_received, bytesReceived)
+ end
+
+ -- log validate timer entry only if its passed
+ if validateTime >= 0 then
+ metrics:timer(validate_request_response_time, validateTime)
+ end
+
+ -- ---------------------------------------------------------------------- --
+ -- ------------- Non-blocked requests related logging --------------- --
+ -- ---------------------------------------------------------------------- --
+
+ -- Choosing log buckets in statsd based on validation success/failure in the gateway
+ if (validateRequestStatus == "200" or validateRequestStatus == "0" or validateRequestStatus == 200 or validateRequestStatus == "na") then
+ local cc_bucket = bucket ..
+ requestPath .. "." ..
+ requestMethod .. "." .. status;
+
+ local cc_response_time = cc_bucket .. ".responseTime";
+ local cc_upstream_response_time = cc_bucket .. ".upstreamResponseTime";
+
+ local hit_count_for_bucket = cc_bucket .. ".count";
+
+
+ --increament the count for all the calls
+ metrics:count(hit_count_for_bucket, bucketCountValue)
+ -- timers
+ if requestTime >= 0 then
+ metrics:timer(cc_response_time, requestTime)
+ end
+ --timer data - update only if its greater than or = zero as its avging all calls
+ if upstreamResponseTime >= 0 then
+ metrics:timer(cc_upstream_response_time, upstreamResponseTime)
+ end
+ return
+ end
+
+ -- ---------------------------------------------------------------------- --
+ -- ------------- Blocked requests related logging --------------- --
+ -- ---------------------------------------------------------------------- --
+
+ local blocked_bucket = bucket .. "_blocked_"
+
+ local code_count_bucket = blocked_bucket .. "." .. validateRequestStatus .. ".count"
+ local bytes_sent_bucket = blocked_bucket .. ".bytesSent"
+ local bytes_received_bucket = blocked_bucket .. ".bytesReceived"
+
+ --increament the count for all the calls
+ metrics:count(code_count_bucket, bucketCountValue)
+
+ --bandwidth data - update only if its greater than zero to sum up all calls
+ if bytesSent > 0 then
+ metrics:count(bytes_sent_bucket, bytesSent)
+ end
+ if bytesReceived > 0 then
+ metrics:count(bytes_received_bucket, bytesReceived)
+ end
+end
+
+
+return M
+
diff --git a/api-gateway-config/scripts/lua/metrics/factory.lua b/api-gateway-config/scripts/lua/metrics/factory.lua
new file mode 100644
index 0000000..d71d504
--- /dev/null
+++ b/api-gateway-config/scripts/lua/metrics/factory.lua
@@ -0,0 +1,37 @@
+-- Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved.
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a
+-- copy of this software and associated documentation files (the "Software"),
+-- to deal in the Software without restriction, including without limitation
+-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
+-- and/or sell copies of the Software, and to permit persons to whom the
+-- Software is furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+--
+-- A factory to initialize the methods that can be used for capturing usage data
+-- This class is provided as an example
+-- User: ddascal
+--
+
+local MetricsCollector = require "metrics.MetricsCollector"
+local collector = MetricsCollector:new()
+
+local function _captureUsageData()
+ return collector:logCurrentRequest()
+end
+
+return {
+ captureUsageData = _captureUsageData
+}
+
diff --git a/api-gateway-config/scripts/python/logger/StatsdLogger.py b/api-gateway-config/scripts/python/logger/StatsdLogger.py
new file mode 100644
index 0000000..af57f76
--- /dev/null
+++ b/api-gateway-config/scripts/python/logger/StatsdLogger.py
@@ -0,0 +1,93 @@
+#
+# Background worker that loads statistics from the Api-Gateway endpoint and sends them to StatsD.
+# Designed to be restarted every minute by a cronjob
+#
+import json, sys, socket, urllib2
+import argparse
+from threading import Timer
+
+class StatsCollectorWorker:
+
+ UDP_MAX_BUFFER_SIZE = 800
+
+ def __init__(self, statsRestEndpoint, statsd_host, statsd_port ):
+ print(("Starting collecting form [%s], sending to StatsD on [%s:%s]") % (gateway_uri, statsd_host, statsd_port) )
+ self.statsRestEndpoint = statsRestEndpoint
+ self.statsd_host = statsd_host
+ self.statsd_port = statsd_port
+ self.udp_buffer = ""
+
+ def getStatsFromUrl(self, url):
+ try:
+ stats_txt = urllib2.urlopen(url).read()
+ return json.loads(stats_txt)
+ except Exception as e:
+ print "Could not read stats", e
+ return None
+
+ def flushBuffer(self):
+ # print "Sending:", self.udp_buffer
+ self.udp_sock.send(self.udp_buffer)
+ self.udp_buffer = ''
+
+ def sentStatsFromCollection(self, metricCollection, metricType):
+ for i, item in enumerate(metricCollection):
+ # print i, item, metricCollection[item]
+ statsd_metric = ('%s:%s|%s' % (item, metricCollection[item],metricType)).encode("utf-8")
+ if self.udp_buffer.__len__() + statsd_metric.__len__() > StatsCollectorWorker.UDP_MAX_BUFFER_SIZE:
+ self.flushBuffer()
+ self.udp_buffer = ''.join([self.udp_buffer, "\n", statsd_metric])
+
+ def sendStats(self, obj):
+ # TODO: split statsd_host by space in order to support sending data to multiple statsd backends
+ try:
+ self.udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ self.udp_sock.connect((self.statsd_host, self.statsd_port))
+ except Exception as e:
+ print "Could not connect to StatsD", e
+ return None
+ try:
+ self.sentStatsFromCollection(obj["timers"], "ms")
+ self.sentStatsFromCollection(obj["counters"], "c")
+ if self.udp_buffer.__len__() > 0:
+ self.flushBuffer()
+ except Exception as e:
+ print "Could not send statistics", e
+ self.udp_sock.close()
+
+ def getAndSendStats(self,nextInterval, maxRuns):
+ # Read the JSON from the gateway's endpoint
+ # TODO: add an exception in case JSON conversion fails
+ obj = self.getStatsFromUrl(self.statsRestEndpoint)
+ if obj is not None:
+ self.sendStats(obj)
+
+ # print ("Stats processed. %s runs left" % (maxRuns))
+ if maxRuns <= 0:
+ return None
+ t = Timer(nextInterval, self.getAndSendStats, [nextInterval, maxRuns - 1])
+ t.start()
+
+
+statsd_host = "api-gateway-graphite"
+statsd_port = int("8125")
+gateway_uri = "http://127.0.0.1/api-gateway-stats"
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-s', '--statsd-host', dest='statsd_host', required=True)
+ parser.add_argument('-p', '--statsd-port', dest='statsd_port')
+ parser.add_argument('-g', '--gateway-uri', dest='gateway_uri')
+ args = parser.parse_args()
+ statsd_host = args.statsd_host or statsd_host
+ statsd_port = int(args.statsd_port or statsd_port)
+ gateway_uri = args.gateway_uri or gateway_uri
+
+collector = StatsCollectorWorker(statsRestEndpoint=gateway_uri, statsd_host=statsd_host, statsd_port=statsd_port)
+# set the collector to run each 3 seconds , max 18 times
+collector.getAndSendStats(nextInterval=3,maxRuns=18)
+
+# Sample StatsD messages
+# pub.publisher_name.consumer.consumer_name.app.application_name.service.service_name.prod.region.useast.request.device_links.POST.200.count
+# pub.publisher_name.consumer.consumer_name.app.application_name.service.service_name.prod.region.useast.request.device_links.POST.200.responseTime
+# pub.publisher_name.consumer.consumer_name.app.application_name.service.service_name.prod.region.useast.request.device_links.POST.200.upstreamResponseTime
+# pub.publisher_name.consumer.consumer_name.app.application_name.service.service_name.prod.region.useast.request.validate_request.GET.200.responseTime
diff --git a/init.sh b/init.sh
index 0ccf8ca..6876a5d 100755
--- a/init.sh
+++ b/init.sh
@@ -1,4 +1,27 @@
#!/bin/sh
+#/*
+# * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
+# *
+# * Permission is hereby granted, free of charge, to any person obtaining a
+# * copy of this software and associated documentation files (the "Software"),
+# * to deal in the Software without restriction, including without limitation
+# * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# * and/or sell copies of the Software, and to permit persons to whom the
+# * Software is furnished to do so, subject to the following conditions:
+# *
+# * The above copyright notice and this permission notice shall be included in
+# * all copies or substantial portions of the Software.
+# *
+# * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# * DEALINGS IN THE SOFTWARE.
+# *
+# */
+
log_level=${LOG_LEVEL:-warn}
marathon_host=$(echo $MARATHON_HOST)
@@ -12,7 +35,17 @@
if [[ -n "${marathon_host}" ]]; then
echo " ... starting Marathon Service Discovery "
touch /var/run/apigateway-config-watcher.lastrun
+ # start marathon's service discovery
while true; do /etc/api-gateway/marathon-service-discovery.sh > /dev/stderr; sleep 5; done &
+ # start simple statsd logger
+ #
+ # ASSUMPTION: there is a graphite app named "api-gateway-graphite" deployed in marathon
+ #
+ while true; do \
+ statsd_host=$(curl -s ${marathon_host}/v2/apps/api-gateway-graphite/tasks | grep 8125 | awk '{for(i=3;i<=NF;++i) printf("%s ", $i) }' | awk '{for(i=1;i<=NF;++i) sub(/\:\d+/,"",$i); print }' ); \
+ if [[ -n "${statsd_host}" ]]; then python /etc/api-gateway/scripts/python/logger/StatsdLogger.py --statsd-host=${statsd_host} > /var/log/api-gateway/statsd-logger.log; fi; \
+ sleep 60; \
+ done &
fi
echo " ... testing configuration "