Add a new API to list loadbalancer statistics

Change-Id: I4521fdaa2f1e2f8f80d5def311ae67937e84479a
Implements: blueprint stats-support
This commit is contained in:
chen-li 2016-08-22 22:34:26 -05:00
parent 8ee4def2b3
commit e6507ed080
10 changed files with 273 additions and 8 deletions

View File

@ -168,6 +168,42 @@ Retrieve details of a load balancer.
}
List Load Balancer Statistics
*****************************
Retrieve the stats of a load balancer.
+----------------+-----------------------------------------------------------+
| Request Type | ``GET`` |
+----------------+-----------------------------------------------------------+
| Endpoint | ``URL/v1/loadbalancers/{lb_id}/stats`` |
+----------------+---------+-------------------------------------------------+
| | Success | 200 |
| Response Codes +---------+-------------------------------------------------+
| | Error | 401, 404, 500 |
+----------------+---------+-------------------------------------------------+
**Response Example**::
{
"loadbalancer": {
"bytes_in": 0,
"bytes_out": 0,
"active_connections": 0,
"total_connections": 0,
"request_errors": 0,
"listeners": [{
"id": "uuid"
"bytes_in": 0,
"bytes_out": 0,
"active_connections": 0,
"total_connections": 0,
"request_errors": 0,
}]
}
}
Create Load Balancer
********************

View File

@ -22,6 +22,7 @@ from wsmeext import pecan as wsme_pecan
from octavia.api.v1.controllers import base
from octavia.api.v1.controllers import listener
from octavia.api.v1.controllers import load_balancer_statistics as lb_stats
from octavia.api.v1.controllers import pool
from octavia.api.v1.types import load_balancer as lb_types
from octavia.common import constants
@ -178,9 +179,9 @@ class LoadBalancersController(base.BaseController):
decides which controller, if any, should control be passed.
"""
context = pecan.request.context.get('octavia_context')
if lb_id and len(remainder) and (remainder[0] == 'listeners' or
remainder[0] == 'pools' or
remainder[0] == 'delete_cascade'):
possible_remainder = ('listeners', 'pools', 'delete_cascade', 'stats')
if lb_id and len(remainder) and (remainder[0] in possible_remainder):
controller = remainder[0]
remainder = remainder[1:]
db_lb = self.repositories.load_balancer.get(context.session,
@ -197,6 +198,9 @@ class LoadBalancersController(base.BaseController):
load_balancer_id=db_lb.id), remainder
elif (controller == 'delete_cascade'):
return LBCascadeDeleteController(db_lb.id), ''
elif (controller == 'stats'):
return lb_stats.LoadBalancerStatisticsController(
loadbalancer_id=db_lb.id), remainder
class LBCascadeDeleteController(LoadBalancersController):

View File

@ -0,0 +1,40 @@
# Copyright 2016 IBM
#
# Licensed 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 pecan
from wsme import types as wtypes
from wsmeext import pecan as wsme_pecan
from octavia.api.v1.controllers import base
from octavia.api.v1.types import load_balancer_statistics as lb_types
from octavia.common import constants
from octavia.common import stats
class LoadBalancerStatisticsController(base.BaseController,
stats.StatsMixin):
def __init__(self, loadbalancer_id):
super(LoadBalancerStatisticsController, self).__init__()
self.loadbalancer_id = loadbalancer_id
@wsme_pecan.wsexpose(
{wtypes.text: lb_types.LoadBalancerStatisticsResponse})
def get(self):
"""Gets a single loadbalancer's statistics details."""
context = pecan.request.context.get('octavia_context')
data_stats = self.get_loadbalancer_stats(
context.session, self.loadbalancer_id)
return {constants.LOADBALANCER: self._convert_db_to_type(
data_stats, lb_types.LoadBalancerStatisticsResponse)}

View File

@ -0,0 +1,50 @@
# Copyright 2016 Blue Box, an IBM Company
#
# Licensed 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.
from wsme import types as wtypes
from octavia.api.v1.types import base
from octavia.api.v1.types import listener_statistics
class ListenerStatistics(listener_statistics.ListenerStatisticsResponse):
id = wtypes.wsattr(wtypes.UuidType())
@classmethod
def from_data_model(cls, data_model, children=False):
ls_stats = super(ListenerStatistics, cls).from_data_model(
data_model, children=children)
ls_stats.id = data_model.listener_id
return ls_stats
class LoadBalancerStatisticsResponse(base.BaseType):
bytes_in = wtypes.wsattr(wtypes.IntegerType())
bytes_out = wtypes.wsattr(wtypes.IntegerType())
active_connections = wtypes.wsattr(wtypes.IntegerType())
total_connections = wtypes.wsattr(wtypes.IntegerType())
request_errors = wtypes.wsattr(wtypes.IntegerType())
listeners = wtypes.wsattr([ListenerStatistics])
@classmethod
def from_data_model(cls, data_model, children=False):
lb_stats = super(LoadBalancerStatisticsResponse, cls).from_data_model(
data_model, children=children)
lb_stats.listeners = [
ListenerStatistics.from_data_model(
listener_dm, children=children)
for listener_dm in data_model.listeners
]
return lb_stats

View File

@ -167,6 +167,28 @@ class ListenerStatistics(BaseDataModel):
return self
class LoadBalancerStatistics(BaseDataModel):
def __init__(self, bytes_in=0, bytes_out=0, active_connections=0,
total_connections=0, request_errors=0, listeners=None):
self.bytes_in = bytes_in
self.bytes_out = bytes_out
self.active_connections = active_connections
self.total_connections = total_connections
self.request_errors = request_errors
self.listeners = listeners or []
def get_stats(self):
stats = {
'bytes_in': self.bytes_in,
'bytes_out': self.bytes_out,
'active_connections': self.active_connections,
'total_connections': self.total_connections,
'request_errors': self.request_errors,
}
return stats
class HealthMonitor(BaseDataModel):
def __init__(self, id=None, project_id=None, pool_id=None, type=None,

View File

@ -29,6 +29,7 @@ class StatsMixin(object):
super(StatsMixin, self).__init__()
self.listener_stats_repo = repo.ListenerStatisticsRepository()
self.repo_amphora = repo.AmphoraRepository()
self.repo_loadbalancer = repo.LoadBalancerRepository()
def get_listener_stats(self, session, listener_id):
"""Gets the listener statistics data_models object."""
@ -48,3 +49,17 @@ class StatsMixin(object):
if amp and amp.status == constants.AMPHORA_ALLOCATED:
statistics.active_connections += db_l.active_connections
return statistics
def get_loadbalancer_stats(self, session, loadbalancer_id):
statistics = data_models.LoadBalancerStatistics()
lb_db = self.repo_loadbalancer.get(session, id=loadbalancer_id)
for listener in lb_db.listeners:
data = self.get_listener_stats(session, listener.id)
statistics.bytes_in += data.bytes_in
statistics.bytes_out += data.bytes_out
statistics.request_errors += data.request_errors
statistics.active_connections += data.active_connections
statistics.total_connections += data.total_connections
statistics.listeners.append(data)
return statistics

View File

@ -217,6 +217,7 @@ class UpdateStatsDb(stats.StatsMixin):
def __init__(self):
super(UpdateStatsDb, self).__init__()
self.event_streamer = event_queue.EventStreamerNeutron()
self.repo_listener = repo.ListenerRepository()
def emit(self, info_type, info_id, info_obj):
cnt = update_serializer.InfoContainer(info_type, info_id, info_obj)
@ -272,3 +273,9 @@ class UpdateStatsDb(stats.StatsMixin):
listener_stats = self.get_listener_stats(session, listener_id)
self.emit(
'listener_stats', listener_id, listener_stats.get_stats())
listener_db = self.repo_listener.get(session, id=listener_id)
lb_stats = self.get_loadbalancer_stats(
session, listener_db.load_balancer_id)
self.emit('loadbalancer_stats',
listener_db.load_balancer_id, lb_stats.get_stats())

View File

@ -31,6 +31,7 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
BASE_PATH = '/v1'
LBS_PATH = '/loadbalancers'
LB_PATH = LBS_PATH + '/{lb_id}'
LB_STATS_PATH = LB_PATH + '/stats'
LISTENERS_PATH = LB_PATH + '/listeners'
LISTENER_PATH = LISTENERS_PATH + '/{listener_id}'
LISTENER_STATS_PATH = LISTENER_PATH + '/stats'

View File

@ -0,0 +1,56 @@
# Copyright 2016 IBM
#
# Licensed 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.
from octavia.common import constants
from octavia.tests.functional.api.v1 import base
from oslo_utils import uuidutils
class TestLoadBlancerStatistics(base.BaseAPITest):
FAKE_UUID_1 = uuidutils.generate_uuid()
def setUp(self):
super(TestLoadBlancerStatistics, self).setUp()
self.lb = self.create_load_balancer({})
self.set_lb_status(self.lb.get('id'))
self.listener = self.create_listener(self.lb.get('id'),
constants.PROTOCOL_HTTP, 80)
self.set_lb_status(self.lb.get('id'))
self.lb_path = self.LB_STATS_PATH.format(lb_id=self.lb.get('id'))
self.amphora = self.create_amphora(uuidutils.generate_uuid(),
self.lb.get('id'))
def test_get(self):
ls = self.create_listener_stats(listener_id=self.listener.get('id'),
amphora_id=self.amphora.id)
expected = {
'loadbalancer': {
'bytes_in': ls['bytes_in'],
'bytes_out': ls['bytes_out'],
'active_connections': ls['active_connections'],
'total_connections': ls['total_connections'],
'request_errors': ls['request_errors'],
'listeners': [
{'id': self.listener.get('id'),
'bytes_in': ls['bytes_in'],
'bytes_out': ls['bytes_out'],
'active_connections': ls['active_connections'],
'total_connections': ls['total_connections'],
'request_errors': ls['request_errors']}]
}
}
response = self.get(self.lb_path)
response_body = response.json
self.assertEqual(expected, response_body)

View File

@ -24,6 +24,7 @@ import sqlalchemy
from octavia.common import constants
from octavia.common import data_models
from octavia.controller.healthmanager import update_db
from octavia.db import models as db_models
from octavia.tests.unit import base
@ -468,14 +469,33 @@ class TestUpdateStatsDb(base.TestCase):
total_connections=random.randrange(1000000000),
request_errors=random.randrange(1000000000))
self.sm.get_listener_stats = mock.MagicMock(
return_value=self.listener_stats)
self.sm.get_listener_stats = mock.MagicMock()
self.sm.get_listener_stats.return_value = self.listener_stats
self.loadbalancer_id = uuidutils.generate_uuid()
self.amphora_id = uuidutils.generate_uuid()
self.listener_id = uuidutils.generate_uuid()
self.listener = db_models.Listener(
load_balancer_id=self.loadbalancer_id)
self.listener_repo = mock.MagicMock()
self.sm.repo_listener = self.listener_repo
self.sm.repo_listener.get.return_value = self.listener
self.loadbalancer_repo = mock.MagicMock()
self.sm.repo_loadbalancer = self.loadbalancer_repo
self.loadbalancer = db_models.LoadBalancer(
id=self.loadbalancer_id,
listeners=[self.listener])
self.loadbalancer_repo.get.return_value = self.loadbalancer
@mock.patch('octavia.db.api.get_session')
def test_update_stats(self, session):
health = {
"id": self.loadbalancer_id,
"id": self.amphora_id,
"listeners": {
self.listener_id: {
"status": constants.OPEN,
@ -501,13 +521,13 @@ class TestUpdateStatsDb(base.TestCase):
self.sm.update_stats(health)
self.listener_stats_repo.replace.assert_called_once_with(
'blah', self.listener_id, self.loadbalancer_id,
'blah', self.listener_id, self.amphora_id,
bytes_in=self.listener_stats.bytes_in,
bytes_out=self.listener_stats.bytes_out,
active_connections=self.listener_stats.active_connections,
total_connections=self.listener_stats.total_connections,
request_errors=self.listener_stats.request_errors)
self.event_client.cast.assert_called_once_with(
self.event_client.cast.assert_any_call(
{}, 'update_info', container={
'info_type': 'listener_stats',
'info_id': self.listener_id,
@ -519,3 +539,17 @@ class TestUpdateStatsDb(base.TestCase):
self.listener_stats.active_connections,
'bytes_out': self.listener_stats.bytes_out,
'request_errors': self.listener_stats.request_errors}})
self.event_client.cast.assert_any_call(
{}, 'update_info',
container={
'info_type': 'loadbalancer_stats',
'info_id': self.loadbalancer_id,
'info_payload': {
'bytes_in': self.listener_stats.bytes_in,
'total_connections':
self.listener_stats.total_connections,
'active_connections':
self.listener_stats.active_connections,
'bytes_out': self.listener_stats.bytes_out,
'request_errors': self.listener_stats.request_errors}})