From e6507ed08039a79f7cc363502609b47273f40736 Mon Sep 17 00:00:00 2001 From: chen-li Date: Mon, 22 Aug 2016 22:34:26 -0500 Subject: [PATCH] Add a new API to list loadbalancer statistics Change-Id: I4521fdaa2f1e2f8f80d5def311ae67937e84479a Implements: blueprint stats-support --- doc/source/api/octaviaapi.rst | 36 ++++++++++++ octavia/api/v1/controllers/load_balancer.py | 10 +++- .../controllers/load_balancer_statistics.py | 40 +++++++++++++ .../api/v1/types/load_balancer_statistics.py | 50 +++++++++++++++++ octavia/common/data_models.py | 22 ++++++++ octavia/common/stats.py | 15 +++++ octavia/controller/healthmanager/update_db.py | 7 +++ octavia/tests/functional/api/v1/base.py | 1 + .../api/v1/test_load_balancer_statistics.py | 56 +++++++++++++++++++ .../healthmanager/test_update_db.py | 44 +++++++++++++-- 10 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 octavia/api/v1/controllers/load_balancer_statistics.py create mode 100644 octavia/api/v1/types/load_balancer_statistics.py create mode 100644 octavia/tests/functional/api/v1/test_load_balancer_statistics.py diff --git a/doc/source/api/octaviaapi.rst b/doc/source/api/octaviaapi.rst index 011be3c9c0..a69426f847 100644 --- a/doc/source/api/octaviaapi.rst +++ b/doc/source/api/octaviaapi.rst @@ -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 ******************** diff --git a/octavia/api/v1/controllers/load_balancer.py b/octavia/api/v1/controllers/load_balancer.py index b50b816cdd..48c1f16ddd 100644 --- a/octavia/api/v1/controllers/load_balancer.py +++ b/octavia/api/v1/controllers/load_balancer.py @@ -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): diff --git a/octavia/api/v1/controllers/load_balancer_statistics.py b/octavia/api/v1/controllers/load_balancer_statistics.py new file mode 100644 index 0000000000..ec349cddf0 --- /dev/null +++ b/octavia/api/v1/controllers/load_balancer_statistics.py @@ -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)} diff --git a/octavia/api/v1/types/load_balancer_statistics.py b/octavia/api/v1/types/load_balancer_statistics.py new file mode 100644 index 0000000000..d299a51a77 --- /dev/null +++ b/octavia/api/v1/types/load_balancer_statistics.py @@ -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 diff --git a/octavia/common/data_models.py b/octavia/common/data_models.py index ad79f8ee14..ad49d641a9 100644 --- a/octavia/common/data_models.py +++ b/octavia/common/data_models.py @@ -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, diff --git a/octavia/common/stats.py b/octavia/common/stats.py index 397b254699..d0e0f1ccb8 100644 --- a/octavia/common/stats.py +++ b/octavia/common/stats.py @@ -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 diff --git a/octavia/controller/healthmanager/update_db.py b/octavia/controller/healthmanager/update_db.py index f41d834e5a..8d773af21c 100644 --- a/octavia/controller/healthmanager/update_db.py +++ b/octavia/controller/healthmanager/update_db.py @@ -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()) diff --git a/octavia/tests/functional/api/v1/base.py b/octavia/tests/functional/api/v1/base.py index 34b822058f..b381dfc3dc 100644 --- a/octavia/tests/functional/api/v1/base.py +++ b/octavia/tests/functional/api/v1/base.py @@ -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' diff --git a/octavia/tests/functional/api/v1/test_load_balancer_statistics.py b/octavia/tests/functional/api/v1/test_load_balancer_statistics.py new file mode 100644 index 0000000000..3e6b4176ea --- /dev/null +++ b/octavia/tests/functional/api/v1/test_load_balancer_statistics.py @@ -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) diff --git a/octavia/tests/unit/controller/healthmanager/test_update_db.py b/octavia/tests/unit/controller/healthmanager/test_update_db.py index 779327a792..837a544864 100644 --- a/octavia/tests/unit/controller/healthmanager/test_update_db.py +++ b/octavia/tests/unit/controller/healthmanager/test_update_db.py @@ -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}})