From 91022f3e8097b8356a7f76061ce1207988ba4351 Mon Sep 17 00:00:00 2001 From: Martin Chacon Piza Date: Mon, 22 Feb 2021 14:53:29 +0100 Subject: [PATCH] Remove project content on master branch This is step 2b of repository deprecation process as described in [1]. [1] https://docs.openstack.org/project-team-guide/repository.html#step-2b-remove-project-content Change-Id: Ie446c1cd447789d189a9e723fff9ee783dd3cf4f --- .coveragerc | 7 - .gitignore | 39 - .stestr.conf | 4 - .zuul.yaml | 62 - CONTRIBUTING.rst | 21 - HACKING.rst | 5 - LICENSE | 175 -- README.deprecated.md | 193 -- README.rst | 80 +- api-guide/source/conf.py | 252 -- api-guide/source/general_info.rst | 50 - api-guide/source/index.rst | 70 - api-guide/source/logs.rst | 18 - api-ref/source/conf.py | 247 -- api-ref/source/healthcheck.inc | 54 - api-ref/source/http_codes.yaml | 56 - api-ref/source/index.rst | 35 - api-ref/source/locale/.gitkeep | 0 api-ref/source/log.inc | 73 - api-ref/source/logs.inc | 75 - api-ref/source/parameters.yaml | 50 - api-ref/source/version.inc | 19 - config-generator/README.md | 13 - config-generator/monasca-log-api.conf | 8 - config-generator/policy.conf | 4 - contrib/post_test_hook.sh | 103 - devstack/README.md | 119 - devstack/Vagrantfile | 123 - devstack/files/apache-log-api.template | 25 - .../files/elasticsearch/elasticsearch.yml | 360 --- .../grafana/dashboards.d/09-monasca.json | 2138 ----------------- .../files/grafana/dashboards.d/20-kibana.json | 624 ----- .../files/grafana/dashboards.d/21-logapi.json | 624 ----- .../dashboards.d/22-logtransformer.json | 624 ----- .../grafana/dashboards.d/23-logtmetrics.json | 624 ----- .../grafana/dashboards.d/24-logpersister.json | 624 ----- devstack/files/grafana/grafana.py | 115 - devstack/files/kibana/kibana.yml | 78 - devstack/files/monasca-agent/elastic.yaml | 3 - devstack/files/monasca-agent/http_check.yaml | 27 - devstack/files/monasca-agent/process.yaml | 143 -- devstack/files/monasca-log-agent/agent.conf | 47 - .../monasca-log-metrics/log-metrics.conf | 81 - .../monasca-log-persister/persister.conf | 71 - .../monasca-log-transformer/transformer.conf | 87 - devstack/lib/config.sh | 107 - devstack/lib/util.sh | 56 - devstack/plugin.sh | 862 ------- devstack/settings | 60 - doc/api_samples/v2/req_json.json | 4 - doc/api_samples/v2/req_text.txt | 1 - doc/api_samples/v3/req_global_dims.json | 22 - doc/api_samples/v3/req_multiple_logs.json | 26 - doc/api_samples/v3/req_single_log.json | 12 - doc/source/.gitignore | 1 - doc/source/_static/.gitkeep | 0 doc/source/_static/images/architecture.png | Bin 169988 -> 0 bytes doc/source/_static/images/architecture.svg | 988 -------- doc/source/admin/index.rst | 6 - doc/source/cli/index.rst | 6 - doc/source/conf.py | 281 --- doc/source/configuration/configuring.rst | 160 -- doc/source/configuration/files.rst | 99 - doc/source/configuration/index.rst | 17 - doc/source/configuration/options.rst | 8 - doc/source/configuration/sample.rst | 51 - doc/source/contributor/.gitignore | 3 - doc/source/contributor/code.rst | 18 - doc/source/contributor/index.rst | 30 - doc/source/contributor/tox.rst | 77 - doc/source/deprecation_note.inc | 6 - doc/source/glossary.rst | 3 - doc/source/index.rst | 50 - doc/source/install/index.rst | 6 - doc/source/user/index.rst | 6 - docker/Dockerfile | 57 - docker/README.rst | 85 - docker/build_image.sh | 150 -- docker/health_check.py | 45 - docker/log-api-gunicorn.conf.j2 | 13 - docker/log-api-logging.conf.j2 | 47 - docker/log-api-paste.ini.j2 | 53 - docker/monasca-log-api.conf.j2 | 42 - docker/start.sh | 46 - documentation/monasca-log-api-kafka.md | 142 -- documentation/monasca-log-api-metrics.md | 108 - documentation/monasca-log-api-spec.md | 319 --- etc/monasca/log-api-logging.conf | 34 - etc/monasca/log-api-paste.ini | 78 - etc/monasca/log-api-uwsgi.ini | 25 - lower-constraints.txt | 87 - monasca_log_api/__init__.py | 0 monasca_log_api/app/__init__.py | 0 monasca_log_api/app/api.py | 129 - monasca_log_api/app/base/__init__.py | 0 monasca_log_api/app/base/error_handlers.py | 28 - monasca_log_api/app/base/exceptions.py | 38 - monasca_log_api/app/base/log_publisher.py | 250 -- monasca_log_api/app/base/model.py | 119 - monasca_log_api/app/base/request.py | 111 - monasca_log_api/app/base/request_context.py | 36 - monasca_log_api/app/base/validation.py | 267 -- monasca_log_api/app/controller/__init__.py | 0 .../app/controller/api/__init__.py | 0 monasca_log_api/app/controller/api/headers.py | 24 - .../app/controller/api/healthcheck_api.py | 60 - .../app/controller/api/logs_api.py | 88 - .../app/controller/api/versions_api.py | 34 - .../app/controller/healthchecks.py | 60 - monasca_log_api/app/controller/v2/__init__.py | 0 .../app/controller/v2/aid/__init__.py | 0 .../app/controller/v2/aid/service.py | 153 -- monasca_log_api/app/controller/v2/logs.py | 105 - monasca_log_api/app/controller/v3/__init__.py | 0 .../app/controller/v3/aid/__init__.py | 0 .../app/controller/v3/aid/bulk_processor.py | 158 -- .../app/controller/v3/aid/helpers.py | 66 - monasca_log_api/app/controller/v3/logs.py | 113 - monasca_log_api/app/controller/versions.py | 128 - monasca_log_api/app/main.py | 48 - monasca_log_api/app/wsgi.py | 32 - monasca_log_api/common/rest/__init__.py | 0 monasca_log_api/common/rest/exceptions.py | 39 - monasca_log_api/common/rest/utils.py | 115 - monasca_log_api/conf/__init__.py | 70 - monasca_log_api/conf/healthcheck.py | 36 - monasca_log_api/conf/log_publisher.py | 41 - monasca_log_api/conf/monitoring.py | 49 - monasca_log_api/conf/role_middleware.py | 47 - monasca_log_api/conf/service.py | 37 - monasca_log_api/config.py | 83 - monasca_log_api/db/__init__.py | 0 monasca_log_api/db/common/__init__.py | 0 monasca_log_api/db/common/model.py | 43 - monasca_log_api/db/repo/__init__.py | 0 monasca_log_api/db/repo/logs_repository.py | 74 - monasca_log_api/healthcheck/__init__.py | 1 - monasca_log_api/healthcheck/kafka_check.py | 99 - monasca_log_api/middleware/__init__.py | 0 monasca_log_api/middleware/role_middleware.py | 152 -- monasca_log_api/monitoring/__init__.py | 0 monasca_log_api/monitoring/client.py | 80 - monasca_log_api/monitoring/metrics.py | 47 - monasca_log_api/policies/__init__.py | 73 - monasca_log_api/policies/healthchecks.py | 44 - monasca_log_api/policies/logs.py | 43 - monasca_log_api/policies/versions.py | 38 - monasca_log_api/tests/__init__.py | 0 monasca_log_api/tests/base.py | 208 -- monasca_log_api/tests/test_config.py | 40 - monasca_log_api/tests/test_healthchecks.py | 75 - monasca_log_api/tests/test_kafka_check.py | 71 - monasca_log_api/tests/test_log_publisher.py | 293 --- monasca_log_api/tests/test_logs.py | 275 --- monasca_log_api/tests/test_logs_v3.py | 317 --- monasca_log_api/tests/test_monitoring.py | 52 - monasca_log_api/tests/test_policy.py | 214 -- monasca_log_api/tests/test_request.py | 101 - monasca_log_api/tests/test_role_middleware.py | 227 -- monasca_log_api/tests/test_service.py | 479 ---- monasca_log_api/tests/test_v2_v3_compare.py | 116 - monasca_log_api/tests/test_version.py | 21 - monasca_log_api/tests/test_versions.py | 126 - monasca_log_api/version.py | 18 - playbooks/docker-publish.yml | 12 - .../notes/cli_args-6dc5e2d13337b871.yaml | 6 - .../notes/deprecate-60181d946200dff7.yaml | 6 - .../notes/drop-py-2-7-02f3af38da6f6634.yaml | 6 - .../notes/os-docs-bf79803595ac884b.yaml | 9 - .../notes/oslo-policy-e142fa9243a8dcf6.yaml | 5 - .../notes/osloconfiggen-d8a0f0a8d1acb961.yaml | 18 - .../notes/project_tree-3a041cbffc83595a.yaml | 6 - ...standard-config-path-f47c856009b6410d.yaml | 7 - .../notes/uwsgi-0bf04f0ecd9c7522.yaml | 8 - .../notes/zuul_v3-b4138cd73cb6117a.yaml | 3 - releasenotes/source/conf.py | 252 -- releasenotes/source/index.rst | 15 - releasenotes/source/locale/.gitkeep | 0 releasenotes/source/pike.rst | 6 - releasenotes/source/queens.rst | 6 - releasenotes/source/rocky.rst | 6 - releasenotes/source/stein.rst | 6 - releasenotes/source/train.rst | 6 - releasenotes/source/unreleased.rst | 5 - requirements.txt | 19 - setup.cfg | 59 - setup.py | 20 - test-requirements.txt | 21 - tools/bashate.sh | 6 - tox.ini | 158 -- 190 files changed, 12 insertions(+), 19097 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .gitignore delete mode 100644 .stestr.conf delete mode 100644 .zuul.yaml delete mode 100644 CONTRIBUTING.rst delete mode 100644 HACKING.rst delete mode 100644 LICENSE delete mode 100644 README.deprecated.md delete mode 100644 api-guide/source/conf.py delete mode 100644 api-guide/source/general_info.rst delete mode 100644 api-guide/source/index.rst delete mode 100644 api-guide/source/logs.rst delete mode 100644 api-ref/source/conf.py delete mode 100644 api-ref/source/healthcheck.inc delete mode 100644 api-ref/source/http_codes.yaml delete mode 100644 api-ref/source/index.rst delete mode 100644 api-ref/source/locale/.gitkeep delete mode 100644 api-ref/source/log.inc delete mode 100644 api-ref/source/logs.inc delete mode 100644 api-ref/source/parameters.yaml delete mode 100644 api-ref/source/version.inc delete mode 100644 config-generator/README.md delete mode 100644 config-generator/monasca-log-api.conf delete mode 100644 config-generator/policy.conf delete mode 100644 contrib/post_test_hook.sh delete mode 100644 devstack/README.md delete mode 100644 devstack/Vagrantfile delete mode 100644 devstack/files/apache-log-api.template delete mode 100644 devstack/files/elasticsearch/elasticsearch.yml delete mode 100644 devstack/files/grafana/dashboards.d/09-monasca.json delete mode 100644 devstack/files/grafana/dashboards.d/20-kibana.json delete mode 100644 devstack/files/grafana/dashboards.d/21-logapi.json delete mode 100644 devstack/files/grafana/dashboards.d/22-logtransformer.json delete mode 100644 devstack/files/grafana/dashboards.d/23-logtmetrics.json delete mode 100644 devstack/files/grafana/dashboards.d/24-logpersister.json delete mode 100644 devstack/files/grafana/grafana.py delete mode 100644 devstack/files/kibana/kibana.yml delete mode 100644 devstack/files/monasca-agent/elastic.yaml delete mode 100644 devstack/files/monasca-agent/http_check.yaml delete mode 100644 devstack/files/monasca-agent/process.yaml delete mode 100644 devstack/files/monasca-log-agent/agent.conf delete mode 100644 devstack/files/monasca-log-metrics/log-metrics.conf delete mode 100644 devstack/files/monasca-log-persister/persister.conf delete mode 100644 devstack/files/monasca-log-transformer/transformer.conf delete mode 100644 devstack/lib/config.sh delete mode 100644 devstack/lib/util.sh delete mode 100644 devstack/plugin.sh delete mode 100644 devstack/settings delete mode 100644 doc/api_samples/v2/req_json.json delete mode 100644 doc/api_samples/v2/req_text.txt delete mode 100644 doc/api_samples/v3/req_global_dims.json delete mode 100644 doc/api_samples/v3/req_multiple_logs.json delete mode 100644 doc/api_samples/v3/req_single_log.json delete mode 100644 doc/source/.gitignore delete mode 100644 doc/source/_static/.gitkeep delete mode 100644 doc/source/_static/images/architecture.png delete mode 100644 doc/source/_static/images/architecture.svg delete mode 100644 doc/source/admin/index.rst delete mode 100644 doc/source/cli/index.rst delete mode 100644 doc/source/conf.py delete mode 100644 doc/source/configuration/configuring.rst delete mode 100644 doc/source/configuration/files.rst delete mode 100644 doc/source/configuration/index.rst delete mode 100644 doc/source/configuration/options.rst delete mode 100644 doc/source/configuration/sample.rst delete mode 100644 doc/source/contributor/.gitignore delete mode 100644 doc/source/contributor/code.rst delete mode 100644 doc/source/contributor/index.rst delete mode 100644 doc/source/contributor/tox.rst delete mode 100644 doc/source/deprecation_note.inc delete mode 100644 doc/source/glossary.rst delete mode 100644 doc/source/index.rst delete mode 100644 doc/source/install/index.rst delete mode 100644 doc/source/user/index.rst delete mode 100644 docker/Dockerfile delete mode 100644 docker/README.rst delete mode 100755 docker/build_image.sh delete mode 100755 docker/health_check.py delete mode 100644 docker/log-api-gunicorn.conf.j2 delete mode 100644 docker/log-api-logging.conf.j2 delete mode 100644 docker/log-api-paste.ini.j2 delete mode 100644 docker/monasca-log-api.conf.j2 delete mode 100644 docker/start.sh delete mode 100644 documentation/monasca-log-api-kafka.md delete mode 100644 documentation/monasca-log-api-metrics.md delete mode 100644 documentation/monasca-log-api-spec.md delete mode 100644 etc/monasca/log-api-logging.conf delete mode 100644 etc/monasca/log-api-paste.ini delete mode 100644 etc/monasca/log-api-uwsgi.ini delete mode 100644 lower-constraints.txt delete mode 100644 monasca_log_api/__init__.py delete mode 100644 monasca_log_api/app/__init__.py delete mode 100644 monasca_log_api/app/api.py delete mode 100644 monasca_log_api/app/base/__init__.py delete mode 100644 monasca_log_api/app/base/error_handlers.py delete mode 100644 monasca_log_api/app/base/exceptions.py delete mode 100644 monasca_log_api/app/base/log_publisher.py delete mode 100644 monasca_log_api/app/base/model.py delete mode 100644 monasca_log_api/app/base/request.py delete mode 100644 monasca_log_api/app/base/request_context.py delete mode 100644 monasca_log_api/app/base/validation.py delete mode 100644 monasca_log_api/app/controller/__init__.py delete mode 100644 monasca_log_api/app/controller/api/__init__.py delete mode 100644 monasca_log_api/app/controller/api/headers.py delete mode 100644 monasca_log_api/app/controller/api/healthcheck_api.py delete mode 100644 monasca_log_api/app/controller/api/logs_api.py delete mode 100644 monasca_log_api/app/controller/api/versions_api.py delete mode 100644 monasca_log_api/app/controller/healthchecks.py delete mode 100644 monasca_log_api/app/controller/v2/__init__.py delete mode 100644 monasca_log_api/app/controller/v2/aid/__init__.py delete mode 100644 monasca_log_api/app/controller/v2/aid/service.py delete mode 100644 monasca_log_api/app/controller/v2/logs.py delete mode 100644 monasca_log_api/app/controller/v3/__init__.py delete mode 100644 monasca_log_api/app/controller/v3/aid/__init__.py delete mode 100644 monasca_log_api/app/controller/v3/aid/bulk_processor.py delete mode 100644 monasca_log_api/app/controller/v3/aid/helpers.py delete mode 100644 monasca_log_api/app/controller/v3/logs.py delete mode 100644 monasca_log_api/app/controller/versions.py delete mode 100644 monasca_log_api/app/main.py delete mode 100644 monasca_log_api/app/wsgi.py delete mode 100644 monasca_log_api/common/rest/__init__.py delete mode 100644 monasca_log_api/common/rest/exceptions.py delete mode 100644 monasca_log_api/common/rest/utils.py delete mode 100644 monasca_log_api/conf/__init__.py delete mode 100644 monasca_log_api/conf/healthcheck.py delete mode 100644 monasca_log_api/conf/log_publisher.py delete mode 100644 monasca_log_api/conf/monitoring.py delete mode 100644 monasca_log_api/conf/role_middleware.py delete mode 100644 monasca_log_api/conf/service.py delete mode 100644 monasca_log_api/config.py delete mode 100644 monasca_log_api/db/__init__.py delete mode 100644 monasca_log_api/db/common/__init__.py delete mode 100644 monasca_log_api/db/common/model.py delete mode 100644 monasca_log_api/db/repo/__init__.py delete mode 100644 monasca_log_api/db/repo/logs_repository.py delete mode 100644 monasca_log_api/healthcheck/__init__.py delete mode 100644 monasca_log_api/healthcheck/kafka_check.py delete mode 100644 monasca_log_api/middleware/__init__.py delete mode 100644 monasca_log_api/middleware/role_middleware.py delete mode 100644 monasca_log_api/monitoring/__init__.py delete mode 100644 monasca_log_api/monitoring/client.py delete mode 100644 monasca_log_api/monitoring/metrics.py delete mode 100644 monasca_log_api/policies/__init__.py delete mode 100644 monasca_log_api/policies/healthchecks.py delete mode 100644 monasca_log_api/policies/logs.py delete mode 100644 monasca_log_api/policies/versions.py delete mode 100644 monasca_log_api/tests/__init__.py delete mode 100644 monasca_log_api/tests/base.py delete mode 100644 monasca_log_api/tests/test_config.py delete mode 100644 monasca_log_api/tests/test_healthchecks.py delete mode 100644 monasca_log_api/tests/test_kafka_check.py delete mode 100644 monasca_log_api/tests/test_log_publisher.py delete mode 100644 monasca_log_api/tests/test_logs.py delete mode 100644 monasca_log_api/tests/test_logs_v3.py delete mode 100644 monasca_log_api/tests/test_monitoring.py delete mode 100644 monasca_log_api/tests/test_policy.py delete mode 100644 monasca_log_api/tests/test_request.py delete mode 100644 monasca_log_api/tests/test_role_middleware.py delete mode 100644 monasca_log_api/tests/test_service.py delete mode 100644 monasca_log_api/tests/test_v2_v3_compare.py delete mode 100644 monasca_log_api/tests/test_version.py delete mode 100644 monasca_log_api/tests/test_versions.py delete mode 100644 monasca_log_api/version.py delete mode 100644 playbooks/docker-publish.yml delete mode 100644 releasenotes/notes/cli_args-6dc5e2d13337b871.yaml delete mode 100644 releasenotes/notes/deprecate-60181d946200dff7.yaml delete mode 100644 releasenotes/notes/drop-py-2-7-02f3af38da6f6634.yaml delete mode 100644 releasenotes/notes/os-docs-bf79803595ac884b.yaml delete mode 100644 releasenotes/notes/oslo-policy-e142fa9243a8dcf6.yaml delete mode 100644 releasenotes/notes/osloconfiggen-d8a0f0a8d1acb961.yaml delete mode 100644 releasenotes/notes/project_tree-3a041cbffc83595a.yaml delete mode 100644 releasenotes/notes/use-standard-config-path-f47c856009b6410d.yaml delete mode 100644 releasenotes/notes/uwsgi-0bf04f0ecd9c7522.yaml delete mode 100644 releasenotes/notes/zuul_v3-b4138cd73cb6117a.yaml delete mode 100644 releasenotes/source/conf.py delete mode 100644 releasenotes/source/index.rst delete mode 100644 releasenotes/source/locale/.gitkeep delete mode 100644 releasenotes/source/pike.rst delete mode 100644 releasenotes/source/queens.rst delete mode 100644 releasenotes/source/rocky.rst delete mode 100644 releasenotes/source/stein.rst delete mode 100644 releasenotes/source/train.rst delete mode 100644 releasenotes/source/unreleased.rst delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 test-requirements.txt delete mode 100644 tools/bashate.sh delete mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 7cb539c4..00000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -branch = True -source = monasca_log_api -omit = monasca_log_api/tests/* - -[report] -ignore_errors = True \ No newline at end of file diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 1a636224..00000000 --- a/.gitignore +++ /dev/null @@ -1,39 +0,0 @@ -*.py[co] -*~ -doc/build/* -dist -build -cover/ -.coverage -.coverage.* -*.egg -*.egg-info -.eggs/ -.stestr/ -.tox/ -MANIFEST -AUTHORS -ChangeLog -monasca-log-api.log -etc/monasca/*.sample - -*.swp -*.iml -.DS_Store -.cache -.classpath -.idea -.project -.target/ -java/debs/* -.settings/ -target -test-output/ -logs/ -*config*.yml -db/config.yml -.venv -.vagrant - -# Logs from devstack directory should not be committed -devstack/*.log diff --git a/.stestr.conf b/.stestr.conf deleted file mode 100644 index 6107277d..00000000 --- a/.stestr.conf +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -test_path=$OS_TEST_PATH -top_dir=./ -group_regex=monasca_log_api\.tests(?:\.|_)([^_]+) diff --git a/.zuul.yaml b/.zuul.yaml deleted file mode 100644 index a41df255..00000000 --- a/.zuul.yaml +++ /dev/null @@ -1,62 +0,0 @@ -- project: - templates: - - check-requirements - - openstack-cover-jobs - - openstack-lower-constraints-jobs - - openstack-python3-victoria-jobs - - publish-openstack-docs-pti - - release-notes-jobs-python3 - check: - jobs: - - monasca-tempest-log-oldapi-python3-influxdb - - build-monasca-docker-image - gate: - queue: monasca - jobs: - - monasca-tempest-log-oldapi-python3-influxdb - post: - jobs: - - publish-monasca-log-api-docker-image - periodic: - jobs: - - publish-monasca-log-api-docker-image - release: - jobs: - - publish-monasca-log-api-docker-image - -- job: - name: publish-monasca-log-api-docker-image - parent: build-monasca-docker-image - post-run: playbooks/docker-publish.yml - required-projects: - - openstack/monasca-common - vars: - publisher: true - secrets: - - doker_hub_login_log_api - -- secret: - name: doker_hub_login_log_api - data: - user: !encrypted/pkcs1-oaep - - mR5DkiEi0lGjpUHgFYlt779KdX0UhPK7+/uJIiawmM5ASJSx90s/DMMDUrPbGc9cpJrOH - zRxBRryUrXuLGpKwQvLAC8uQC3rd9rGrlTVwjszl3nDNFYxKJ3tBMjppG6dBTIo58v6Go - Xo9GdNmaeS3jh6wh9hUqmMlHF4fzaX49GUpZyxTwufA0Y4h/pbq1r7ImbOCLiNUEwxWqH - dPJ9aJMQ5u8qus2r8cs7EHU/KAsFfPEteNtOz3egDSb1/SFYs5oI0VeIs4RvidHe6M56v - vEYx2KjzKKIE4s5TAiU8jU2Qs0RROv+6s7Y23ciqOfFBhVKJg+0PAoge1AwmL6g3FNrW0 - sy0uEslPltkbF3Pae9zFe0VU6ZIxYC62kAIBEJj96zc0TIfFYHx4NRbIvkoKopnun7dqF - 6hg5k8lZ1kN5RMT2nPNemj9esh0eqCBvLjf4ljy7veYLN9M/GSKSP5A+2chBWFxloWuwB - 7PRf9GbDbHA+H+Y6WASK38haHlo9zsoi/D7oJK02LHTwgj4ED3qN34Z9zCVBBr1jN3TUH - 9aVOsm+OT1OsUszLQ3BM1HqBZLszrOxuPoqDQ5CURrxEtzrSnX+fsoLyv1tzT/vVnrbAT - /zJxBXGiiE5/3H0isFkK6GUVwsmnLUFYCq5ye6NyLpwdWfTEx6eQDlO5OIE/Qo= - password: !encrypted/pkcs1-oaep - - DALBrHoOI3iZAKArnbPL59d3Mk1OHeDNREq9CJXs601l70AwPmNnK0y1XixbUoAmB8y9C - hkadR+a2s8Lc2+xCzRvr+4tt8vZ+gjgl77WTs43mpb+DfssdP6s5847t6cnE1WKI7GgCS - 4as41aOz4HDIdLG6Oe5Jq8HYDx+qcBZhQaeCzObLnbmxdDop6SkpjfziV8+TWxag56joE - V7masPLtYofzQpqB0hRhD3RF7T5fW8uOUlKKzSKhjYuwul1sbOKn6izjufxQs0iimhJpI - HkWCPSj9emr889BfO2DR9/OvTjwSdWZmEtup3RAyWcd6JUESa3oLkG1OFqB0vp6hgx9/F - zFXhBxtEyvs1Odj1ecudr7l+zO4kPVG9cIMHHzd7Tlg1XX2O1/+houVmXd3I1GtxwRYZa - B3Q+yecTLleR1coMM4nZgGtbiky5q6zcttTQCxeUEeCpTUWbxhnLPZyXV+pdFMxsLeUjP - 9GcnjuNCgOKHYHyHcXwDoytsqgoTaAS4O+FR5tCmc9dREWsaRCd0dBMy6Xx+ynVJE/1jp - XwtKG/yaFdHyTXwXGQ6ZlW9XnesBejLmetZHx8j7atiMOtKqmM4+N9i0O+1wbl3A2Glxs - K1orRrDJBdLNYVdczq/TUvzl4XBXi+36f1i6auL/WqHUmQy62w1S9WvaqGzEKM= diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index dafe5518..00000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,21 +0,0 @@ -If you would like to contribute to the development of OpenStack, -you must follow the steps documented at: - - http://docs.openstack.org/infra/manual/developers.html#getting-started - -Once those steps have been completed, changes to OpenStack -should be submitted for review via the Gerrit tool, following -the workflow documented at: - - http://docs.openstack.org/infra/manual/developers.html#development-workflow - -Pull requests submitted through GitHub will be ignored. - -Bugs should be filed on Storyboard (using tag **bug**), not GitHub: - - https://storyboard.openstack.org/#!/project/869 - -Additionally, specific guidelines for contributing to Monasca-Log-Api may be found in -Monasca-Log-Api's Documentation: - - https://docs.openstack.org/monasca-log-api/latest/contributor/ diff --git a/HACKING.rst b/HACKING.rst deleted file mode 100644 index 3fb9088d..00000000 --- a/HACKING.rst +++ /dev/null @@ -1,5 +0,0 @@ -monasca-log-api Style Commandments -================================== - -- Step 1: Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ -- Step 2: Read on diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 67db8588..00000000 --- a/LICENSE +++ /dev/null @@ -1,175 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. diff --git a/README.deprecated.md b/README.deprecated.md deleted file mode 100644 index f109f85e..00000000 --- a/README.deprecated.md +++ /dev/null @@ -1,193 +0,0 @@ -Team and repository tags -======================== - -[![Team and repository tags](https://governance.openstack.org/badges/monasca-log-api.svg)](https://governance.openstack.org/reference/tags/index.html) - - - -# Overview - -`monasca-log-api` is a RESTful API server that is designed with a layered architecture [layered architecture](http://en.wikipedia.org/wiki/Multilayered_architecture). - -The full API Specification can be found in [documentation/monasca-log-api-spec.md](documentation/monasca-log-api-spec.md) - -## Monasca-log-api Python - -### Installation - -To install the python api implementation, git clone the source and run the -following command: -```sh - sudo python setup.py install -``` - -### Configuration - -If it installs successfully, you will need to make changes to the following -two files to reflect your system settings, especially where kafka server is -located:: - -```sh - /etc/monasca/log-api-config.conf - /etc/monasca/log-api-config.ini - /etc/monasca/log-api-logging.conf -``` - -Once the configurations are modified to match your environment, you can start -up the server using either Gunicorn or Apache. - -### Start the Server -- for Gunicorn - -The server can be run in the foreground, or as daemons: - -Running the server in foreground mode with Gunicorn: - -```sh - gunicorn -k eventlet --worker-connections=2000 --backlog=1000 - --paste /etc/monasca/log-api-config.ini -``` - -Running the server as daemons with Gunicorn: - -```sh - gunicorn -k eventlet --worker-connections=2000 --backlog=1000 - --paste /etc/monasca/log-api-config.ini -D -``` - -### Start the Server -- for Apache - -To start the server using Apache: create a modwsgi file, -create a modwsgi config file, and enable the wsgi module -in Apache. - -The modwsgi conf file may look something like this, and the site will need to be enabled: - -```sh - Listen myhost:8082 - Listen 127.0.0.1:8082 - - - WSGIDaemonProcess log-api processes=4 threads=4 socket-timeout=120 user=log group=log python-path=/usr/local/lib/python2.7/site-packages - WSGIProcessGroup log-api - WSGIApplicationGroup log-api - WSGIScriptAlias / /var/www/log/log-api.wsgi - - ErrorLog /var/log/log-api/wsgi.log - LogLevel info - CustomLog /var/log/log-api/wsgi-access.log combined - - - Options Indexes FollowSymLinks MultiViews - Require all granted - AllowOverride None - Order allow,deny - allow from all - LimitRequestBody 102400 - - - SetEnv no-gzip 1 - - - -``` - -The wsgi file may look something like this: - -```sh - from monasca_log_api.server import get_wsgi_app - - application = get_wsgi_app(config_base_path='/etc/monasca') -``` - -## Testing - -### Commandline run -To check the server from the commandline: - -```sh - python server.py -``` - -### PEP8 guidelines -To check if the code follows python coding style, run the following command -from the root directory of this project: - -```sh - tox -e pep8 -``` - -### Unit Tests -To run all the unit test cases, run the following command from the root -directory of this project: - -```sh - tox -e py27 - tox -e py35 -``` - -### Coverage -To generate coverage results, run the following command from the root -directory of this project: - -```sh - tox -e cover -``` - -### Building - -To build an installable package, run the following command from the root -directory of this project: - -```sh - python setup.py sdist -``` - -### Documentation - -To generate documentation, run the following command from the root -directory of this project: - -```sh - tox -e docs -``` - -That will create documentation under build folder relative to root of the -project. - -## Architectural layers - -Requests flow through the following architectural layers from top to bottom: - -* Resource - * Serves as the entrypoint into the service. - * Responsible for handling web service requests, and performing structural request validation. -* Application - * Responsible for providing application level implementations for specific use cases. -* Domain - * Contains the technology agnostic core domain model and domain service definitions. - * Responsible for upholding invariants and defining state transitions. -* Infrastructure - * Contains technology specific implementations of domain services. - -## Documentation - -* API Specification: [/documentation/monasca-log-api-spec.md](/documentation/monasca-log-api-spec.md). -* Kafka communication: [/documentation/monasca-log-api-kafka.md](/documentation/monasca-log-api-kafka.md). -* API Monitoring: [/documentation/monasca-log-api-metrics.md](/documentation/monasca-log-api-metrics.md). - -# License - - # Copyright 2015 kornicameister@gmail.com - # Copyright 2015-2017 FUJITSU LIMITED - # - # 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. diff --git a/README.rst b/README.rst index 762e301d..c00729d0 100644 --- a/README.rst +++ b/README.rst @@ -1,73 +1,17 @@ -======================== -Team and repository tags -======================== - -.. image:: https://governance.openstack.org/tc/badges/monasca-log-api.svg - :target: https://governance.openstack.org/tc/reference/tags/index.html - -.. Change things from this point on - OpenStack Monasca-Log-Api ========================= -.. important:: +This project is no longer maintained. + +The contents of this repository are still available in the Git +source code management system. To see the contents of this +repository before it reached its end of life, please check out the +previous commit with "git checkout HEAD^1". - This API is deprecated. Last maintained release is OpenStack Train. - Please use `Monasca API `_ - for newer versions. This repository is kept only for maintenance purposes. +This API is deprecated. Last maintained release is OpenStack Train. +Please use `Monasca API `_ +for newer versions. This repository is kept only for maintenance purposes. -OpenStack Monasca-Log-Api provides RESTful Api to collect logs -from the OpenStack cloud. - -OpenStack Monasca-Log-Api is distributed under the terms of the Apache -License, Version 2.0. The full terms and conditions of this -license are detailed in the LICENSE file. - -Api ---- - -To learn how to use Monasca-Log-Api, consult the documentation -available online at: - -* `Api Guide `_ -* `Api Ref `_ - -For more information on OpenStack Apis, SDKs and CLIs, -please see: - -* `OpenStack Application Development `_ -* `OpenStack Developer Documentation `_ - -Developers ----------- - -For information on how to contribute to Monasca-Log-Api, please see the -contents of the CONTRIBUTING.rst. - -Any new code must follow the development guidelines detailed -in the HACKING.rst file, and pass all unit tests as well as linters. - -Further developer focused documentation is available at: - -* `Monasca-Log-Api `_ - -Operators ---------- - -To learn how to deploy and configure OpenStack Monasca-Log-Api, consult the -documentation available online at: - -* `Installation `_ -* `Configuration `_ - -Bug tracking ------------- - -In the unfortunate event that bugs are discovered, they should -be reported to the appropriate bug tracker. If you obtained -the software from a 3rd party operating system vendor, it is -often wise to use their own bug tracker for reporting problems. -In all other cases use the master OpenStack bug tracker, -available at: - -* `Storyboard `_ +For any further questions, please email +openstack-discuss@lists.openstack.org or join #openstack-monasca on +Freenode. diff --git a/api-guide/source/conf.py b/api-guide/source/conf.py deleted file mode 100644 index ca432ea1..00000000 --- a/api-guide/source/conf.py +++ /dev/null @@ -1,252 +0,0 @@ -# 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. -# -# Key Manager API documentation build configuration file -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.6' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'openstackdocstheme' -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index' - -# General details about project -openstackdocs_repo_name = 'openstack/monasca-log-api' -openstackdocs_auto_name = False -project = 'Monasca Log API Guide' -openstackdocs_bug_project = 'monasca-log-api' -openstackdocs_bug_tag = 'api-guide' -copyright = '2014, OpenStack Foundation' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'native' - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'openstackdocs' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = 'API Guide' - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = [] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# html_extra_path = [] - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'monascalogapi-api-guide' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # 'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'MonascaLogApiAPI.tex', u'Key Manager API Documentation', - u'OpenStack Foundation', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'monascalogapiapi', u'Monasca Log API Documentation', - [u'OpenStack Foundation'], 1) -] - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'MonascaLogApiAPIGuide', u'Monasca Log API Guide', - u'OpenStack Foundation', 'APIGuide', - 'This guide teaches OpenStack Monasca Log service users concepts about ' - 'managing keys in an OpenStack cloud with the Monasca Log API.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False - -# -- Options for Internationalization output ------------------------------ -locale_dirs = ['locale/'] - -# -- Options for PDF output -------------------------------------------------- - -pdf_documents = [ - ('index', u'MonascaLogApiAPIGuide', u'Key Manager API Guide', u'OpenStack ' - 'contributors') -] diff --git a/api-guide/source/general_info.rst b/api-guide/source/general_info.rst deleted file mode 100644 index ae6cad6a..00000000 --- a/api-guide/source/general_info.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. - Copyright 2014-2017 Fujitsu LIMITED - - 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. - -======================== -Monasca Log API Concepts -======================== - -The Monasca Log API is defined as a RESTful HTTP service. The API -takes advantage of all aspects of the HTTP protocol (methods, URIs, -media types, response codes, etc.) and providers are free to use -existing features of the protocol such as caching, persistent -connections, and content compression among others. - -Providers can return information identifying requests in HTTP response -headers, for example, to facilitate communication between the provider -and client applications. - -Monasca LOG is a service that provides log collection capabilities over cloud. - -User Concepts -============= - -To use the Monasca Log API effectively, you should understand several -key concepts: - -- **Log** - -- **Dimensions** - -Relationship with Metric API -============================ - -The Monasca Log API follow similar concept as Monasca Metric API. -Both are using the same meta-like language to describie entities that are -sent over wire. Below list enumerates those meta properties: - -- dimensions -- meta_value diff --git a/api-guide/source/index.rst b/api-guide/source/index.rst deleted file mode 100644 index 111fef95..00000000 --- a/api-guide/source/index.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. - Copyright 2014-2017 Fujitsu LIMITED - - 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. - -=============== -Monasca Log API -=============== - -The monasca-log-api project has a RESTful HTTP service called the -Monasca Log API. Through this API users are able to send logs from entire -cloud. - -This guide covers the concepts in the Monasca Log API. -For a full reference listing, please see: -`Monasca Log API Reference `__. - -We welcome feedback, comments, and bug reports at -`storyboard/monasca `__. - -Intended audience -================= - -This guide assists software developers who want to develop applications -using the Monasca Log API. To use this information, you should -have access to an account from an OpenStack Compute provider, or have -access to your own deployment, and you should also be familiar with the -following concepts: - -* Monasca services -* RESTful HTTP services -* HTTP/1.1 -* JSON data serialization formats - -End User and Operator APIs -========================== - -The Log API includes all end user and operator API calls. -The API works with keystone and, at the monent, uses custom RBAC to -enforce API security. - -API Versions -============ - -Following the Newton release, every Nova deployment should have -the following endpoints: - -* / - list of available versions -* /v2.0 - the first version, permitted only single log to be send per request -* /v3.0 - the next version, allows sending multiple logs at once - -Contents -======== - -.. toctree:: - :maxdepth: 2 - - general_info - logs - diff --git a/api-guide/source/logs.rst b/api-guide/source/logs.rst deleted file mode 100644 index db0306f7..00000000 --- a/api-guide/source/logs.rst +++ /dev/null @@ -1,18 +0,0 @@ -.. - Copyright 2014-2017 Fujitsu LIMITED - - 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. - -==== -Logs -==== diff --git a/api-ref/source/conf.py b/api-ref/source/conf.py deleted file mode 100644 index a056160b..00000000 --- a/api-ref/source/conf.py +++ /dev/null @@ -1,247 +0,0 @@ -# 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. -# -# Key Manager API documentation build configuration file -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.6' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'os_api_ref', - 'openstackdocstheme' -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index' - -# General details about project -openstackdocs_repo_name = 'openstack/monasca-log-api' -openstackdocs_auto_name = False -project = 'Monasca Log Ref Guide' -openstackdocs_bug_project = 'monasca-log-api' -openstackdocs_bug_tag = 'api-ref' -copyright = '2014, OpenStack Foundation' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'native' - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'openstackdocs' - -# To use the API Reference sidebar dropdown menu, -# uncomment the html_theme_options parameter. The theme -# variable, sidebar_dropdown, should be set to `api_ref`. -# Otherwise, the list of links for the User and Ops docs -# appear in the sidebar dropdown menu. -html_theme_options = {"sidebar_dropdown": "api_ref", - "sidebar_mode": "toc"} - -# A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = 'API Ref' - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = [] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# html_extra_path = [] - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'monascalogapi-api-ref' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # 'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'MonascaLogApi.tex', u'Monasca Log API Documentation', - u'OpenStack Foundation', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'monascalogapi', u'Monasca Log API Documentation', - [u'OpenStack Foundation'], 1) -] - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'MonascaLogAPI', u'Monasca Log API Documentation', - u'OpenStack Foundation', 'MonascaLogAPI', 'Monasca Log API', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False - -# -- Options for Internationalization output ------------------------------ -locale_dirs = ['locale/'] diff --git a/api-ref/source/healthcheck.inc b/api-ref/source/healthcheck.inc deleted file mode 100644 index b1bfdb36..00000000 --- a/api-ref/source/healthcheck.inc +++ /dev/null @@ -1,54 +0,0 @@ -.. -*- rst -*- -.. - Copyright 2017 Fujitsu LIMITED - - 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. - -=========== -Healthcheck -=========== - -The *Monasca Log API* comes with a built-in health check mechanism. -It is available in two flavors (simple and complex). - -Simple check -============ - -The simple check only returns response only if API is up -and running. It does not return any data because it is accessible only -for ```HEAD``` requests. - -.. rest_method:: HEAD /healthcheck - -.. rest_status_code:: success http_codes.yaml - - - 204: la_up - -Complex check -============= - -# TODO(trebskit) add note to api-guide about peripheral checks - -The complex check not only returns a response with success code if API -is up and running but it also verifies if peripheral components are -in expected condition. - -.. rest_method:: GET /healthcheck - -.. rest_status_code:: success http_codes.yaml - - - 200: la_up - -.. rest_status_code:: error http_codes.yaml - - - 503: no_health diff --git a/api-ref/source/http_codes.yaml b/api-ref/source/http_codes.yaml deleted file mode 100644 index 9e12b04b..00000000 --- a/api-ref/source/http_codes.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2017 Fujitsu LIMITED - -200: - la_up: | - API is up and running. -204: - default: | - Normal response code, everything went as expected (or even better). - la_up: | - API is up and running. -400: - default: | - Sent data was malformed. -401: - default: | - User must authenticate before making a request. -403: - default: | - Policy does not allow current user to do this operation. -411: - default: | - Content-Length header was not found in request. -413: - default: | - Sent body is too large to be processed. -422: - default: | - Send data could not be processed properly. - no_dims: | - Dimensions are required. - dim_name_too_long: | - Dimension name {name} must be 255 characters or less. - dim_name_underscore: | - Dimension name {name} cannot start with underscore (_). - dim_name_forbidden_chars: | - Dimension name {name} may not contain: "> < = { } ( ) \' " , ; &". - dim_name_empty: | - Dimension name cannot be empty. - dim_value_too_long: | - Dimension value {value} must be 255 characters or less. - dim_value_empty: | - Dimension value cannot be empty. - app_type_too_long: | - Application type {type} must be {length} characters or less. - log_no_msg: | - Log property must have message. - bad_envelope: | - Failed to create an envelope. - -503: - default: | - The server is currently unable to handle the request due to a - temporary overload or scheduled maintenance. - This will likely be alleviated after some delay. - no_health: - API is running but there are problems with peripheral components. diff --git a/api-ref/source/index.rst b/api-ref/source/index.rst deleted file mode 100644 index 807149a7..00000000 --- a/api-ref/source/index.rst +++ /dev/null @@ -1,35 +0,0 @@ -:tocdepth: 2 - -.. - Copyright 2014-2017 Fujitsu LIMITED - - 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. - -======================== -Monasca Log Service APIs -======================== - -.. rest_expand_all:: - -.. include:: logs.inc -.. include:: version.inc -.. include:: healthcheck.inc - -=============== -Deprecated APIs -=============== - -This section contains the reference for APIs that are -depracted in the Monasca Log Service - -.. include:: log.inc diff --git a/api-ref/source/locale/.gitkeep b/api-ref/source/locale/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/api-ref/source/log.inc b/api-ref/source/log.inc deleted file mode 100644 index 881ffab5..00000000 --- a/api-ref/source/log.inc +++ /dev/null @@ -1,73 +0,0 @@ -.. -*- rst -*- -.. - Copyright 2017 Fujitsu LIMITED - - 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. - -==== -Log -==== - -Accepts just a single log entry sent from log-agent of another client. -Can work with logs specified as json (application/json) and text (text/plain) - -Send logs -========= - -.. rest_method:: POST /v2.0/log/single - -Accepts single log entry. - -.. rest_status_code:: success http_codes.yaml - - - 204 - -.. rest_status_code:: error http_codes.yaml - - - 422: no_dims - - 422: app_type_too_long - - 422: dim_name_too_long - - 422: dim_name_underscore - - 422: dim_name_forbidden_chars - - 422: dim_name_empty - - 422: dim_value_too_long - - 422: dim_value_empty - - 422: log_no_msg - - 422: bad_envelope - - 503 - -Request -------- - - .. rest_parameters:: parameters.yaml - - - log_json: log_json - - log_text: log_text - - X_Dimensions: X_Dimensions - - X_Application_Type: X_Application_Type - -**Example 1: Simple request with single log (json)** - -.. literalinclude:: ../../doc/api_samples/v2/req_json.json - :language: javascript - -**Example 2: Simple request with single log (text)** - -.. literalinclude:: ../../doc/api_samples/v2/req_text.txt - :language: text - - -Response --------- - -No body content is returned on a successful POST diff --git a/api-ref/source/logs.inc b/api-ref/source/logs.inc deleted file mode 100644 index 5b47d07d..00000000 --- a/api-ref/source/logs.inc +++ /dev/null @@ -1,75 +0,0 @@ -.. -*- rst -*- -.. - Copyright 2017 Fujitsu LIMITED - - 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. - -==== -Logs -==== - -Accepts logs send from log-agents. Logs are basically raw lines, -as collected from physical resources, enriched with dimensions. - -Send logs -========= - -.. rest_method:: POST /v3.0/logs - -Accepts multiple logs (i.e. bulk mode). Each log can be enriched with set -of dimensions. If necessary some of the dimensions can be specified as global -dimensions (that is particularly useful, to make request smaller, -if there is a lot of duplicates among each log entry dimensions) - -.. rest_status_code:: success http_codes.yaml - - - 204 - -.. rest_status_code:: error http_codes.yaml - - - 400 - - 401 - - 403 - - 411 - - 413 - - 422: log_no_msg - - 422: bad_envelope - - 503 - -Request -------- - - .. rest_parameters:: parameters.yaml - - - dimensions: dimensions - - logs: logs - -**Example 1: Simple request with single log** - -.. literalinclude:: ../../doc/api_samples/v3/req_single_log.json - :language: javascript - -**Example 2: Send multiple logs at once** - -.. literalinclude:: ../../doc/api_samples/v3/req_multiple_logs.json - :language: javascript - -**Example 3: Specify global dimensions for each log entry** - -.. literalinclude:: ../../doc/api_samples/v3/req_global_dims.json - :language: javascript - -Response --------- - -No body content is returned on a successful POST diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml deleted file mode 100644 index dc9038ca..00000000 --- a/api-ref/source/parameters.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2017 Fujitsu LIMITED - -# header params -X_Application_Type: - description: | - A single string value representing the application that has generated - given log entry - in: header - required: true - required: true - type: string - min_version: 2.0 -X_Dimensions: - description: | - A dictionary consisting of (key, value) pairs used to uniquely - identify a log. - in: header - required: true - type: dict - min_version: 2.0 - -# body params -dimensions: - description: | - Dimensions sent in request body are known as global dimensions. - Each dimension applies to each log entry sent in a bulk request. - Dimensions are simple map (thus having key-value structure). - in: body - required: false - type: object - min_version: 3.0 -log_json: - description: | - Single log entry specified as application/json - in: body - required: true - type: object -log_text: - description: | - Single log entry specified as text/plain - in: body - required: true - type: string -logs: - description: | - Array containing each log entry, sent in bulk request. - in: body - required: true - type: object - min_version: 3.0 diff --git a/api-ref/source/version.inc b/api-ref/source/version.inc deleted file mode 100644 index 6bce887e..00000000 --- a/api-ref/source/version.inc +++ /dev/null @@ -1,19 +0,0 @@ -.. -*- rst -*- -.. - Copyright 2017 Fujitsu LIMITED - - 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. - -======= -Version -======= diff --git a/config-generator/README.md b/config-generator/README.md deleted file mode 100644 index 477d8a36..00000000 --- a/config-generator/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# config-generator - -To generate sample configuration execute - -```sh -tox -e genconfig -``` - -To generate the sample policies execute - -```sh -tox -e genpolicy -``` diff --git a/config-generator/monasca-log-api.conf b/config-generator/monasca-log-api.conf deleted file mode 100644 index b946a254..00000000 --- a/config-generator/monasca-log-api.conf +++ /dev/null @@ -1,8 +0,0 @@ -[DEFAULT] -output_file = etc/monasca/monasca-log-api.conf.sample -wrap_width = 79 -format = ini -summarize = True -namespace = monasca_log_api -namespace = oslo.log -namespace = oslo.policy diff --git a/config-generator/policy.conf b/config-generator/policy.conf deleted file mode 100644 index 075c2b99..00000000 --- a/config-generator/policy.conf +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -output_file = etc/monasca/log-api.policy.yaml.sample -format = yaml -namespace = monasca_log_api \ No newline at end of file diff --git a/contrib/post_test_hook.sh b/contrib/post_test_hook.sh deleted file mode 100644 index b41cc929..00000000 --- a/contrib/post_test_hook.sh +++ /dev/null @@ -1,103 +0,0 @@ -# -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP -# (C) Copyright 2016-2017 FUJITSU LIMITED -# -# 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. - -# Sleep some time until all services are starting -sleep 6 - -function load_devstack_utilities { - source $BASE/new/devstack/stackrc - source $BASE/new/devstack/functions - source $BASE/new/devstack/openrc admin admin - - # print OS_ variables - env | grep OS_ -} - -function setup_monasca_log { - local constraints="-c $REQUIREMENTS_DIR/upper-constraints.txt" - - pushd $TEMPEST_DIR - sudo -EH pip install $constraints -r requirements.txt -r test-requirements.txt - popd; - - pushd $MONASCA_LOG_API_DIR - sudo -EH pip install $constraints -r requirements.txt -r test-requirements.txt - sudo -EH python setup.py install - popd; -} - -function set_tempest_conf { - - local conf_file=$TEMPEST_DIR/etc/tempest.conf - pushd $TEMPEST_DIR - oslo-config-generator \ - --config-file tempest/cmd/config-generator.tempest.conf \ - --output-file $conf_file - popd - - cp -f $DEST/tempest/etc/logging.conf.sample $DEST/tempest/etc/logging.conf - - # set identity section - iniset $conf_file identity admin_domain_scope True - - iniset $conf_file identity user_unique_last_password_count 2 - iniset $conf_file identity user_locakout_duration 5 - iniset $conf_file identity user_lockout_failure_attempts 2 - - iniset $conf_file identity uri $OS_AUTH_URL/v2.0 - iniset $conf_file identity uri_v3 $OS_AUTH_URL/v3 - iniset $conf_file identity auth_version v$OS_IDENTITY_API_VERSION - iniset $conf_file identity region $OS_REGION_NAME - - # NOTE(trebskit) we're processing a lot here, increase http timeout - iniset $conf_file identity-feature-enabled http_timeout 120 - - # set auth section - iniset $conf_file auth use_dynamic_credentials True - iniset $conf_file auth admin_username $OS_USERNAME - iniset $conf_file auth admin_password $OS_PASSWORD - iniset $conf_file auth admin_domain_name $OS_PROJECT_DOMAIN_ID - iniset $conf_file auth admin_project_name $OS_PROJECT_NAME - iniset $conf_file auth tempest_roles monasca-user - - # set monitoring section - iniset $conf_file monitoring kibana_version 4.6.3 -} - -function function_exists { - declare -f -F $1 > /dev/null -} - -if ! function_exists echo_summary; then - function echo_summary { - echo $@ - } -fi - -echo_summary "monasca-log's post_test_hook.sh was called..." -(set -o posix; set) - -# save ref to monasca-api dir -export MONASCA_LOG_API_DIR="$BASE/new/monasca-log-api" -export TEMPEST_DIR="$BASE/new/tempest" - -sudo chown -R "${USER}":stack $MONASCA_LOG_API_DIR -sudo chown -R "${USER}":stack $TEMPEST_DIR - -load_devstack_utilities -setup_monasca_log -set_tempest_conf diff --git a/devstack/README.md b/devstack/README.md deleted file mode 100644 index d6bd2b5f..00000000 --- a/devstack/README.md +++ /dev/null @@ -1,119 +0,0 @@ -# Monasca Log Management DevStack Plugin - -The Monasca Log Management DevStack plugin currently only works on Ubuntu 14.04 (Trusty). -More Linux Distributions will be supported in the future. - -Monasca Log Management Devstack plugin requires Monasca Devstack plugin. -Running the Monasca DevStack plugin and Monasca Log Management Devstack plugin requires a machine with 14GB of RAM. - -Directions for installing and running Devstack can be found here: -``` -https://docs.openstack.org/devstack/latest/ -``` - -To run Monasca Log Management in DevStack, do the following three steps. - -1. Clone the DevStack repo. - -``` -git clone https://opendev.org/openstack/devstack -``` - -2. Add the following to the DevStack local.conf file in the root of the devstack directory. You may - need to create the local.conf if it does not already exist. - -``` -[[local|localrc]] -MYSQL_PASSWORD=secretmysql -DATABASE_PASSWORD=secretdatabase -RABBIT_PASSWORD=secretrabbit -ADMIN_PASSWORD=secretadmin -SERVICE_PASSWORD=secretservice -SERVICE_TOKEN=111222333444 - -LOGFILE=$DEST/logs/stack.sh.log -LOGDIR=$DEST/logs -LOG_COLOR=False - -# The following two variables allow switching between Java and Python for the implementations -# of the Monasca API and the Monasca Persister. If these variables are not set, then the -# default is to install the Python implementations of both the Monasca API and the Monasca Persister. - -# Uncomment one of the following two lines to choose Java or Python for the Monasca API. -MONASCA_API_IMPLEMENTATION_LANG=${MONASCA_API_IMPLEMENTATION_LANG:-java} -# MONASCA_API_IMPLEMENTATION_LANG=${MONASCA_API_IMPLEMENTATION_LANG:-python} - -# Uncomment of the following two lines to choose Java or Python for the Monasca Pesister. -MONASCA_PERSISTER_IMPLEMENTATION_LANG=${MONASCA_PERSISTER_IMPLEMENTATION_LANG:-java} -# MONASCA_PERSISTER_IMPLEMENTATION_LANG=${MONASCA_PERSISTER_IMPLEMENTATION_LANG:-python} - -# Uncomment one of the following two lines to choose either InfluxDB or Vertica. -MONASCA_METRICS_DB=${MONASCA_METRICS_DB:-influxdb} -# MONASCA_METRICS_DB=${MONASCA_METRICS_DB:-vertica} - -# This line will enable all of Monasca. -enable_plugin monasca-api https://opendev.org/openstack/monasca-api -enable_plugin monasca-log-api https://opendev.org/openstack/monasca-log-api.git -``` - -3. Run './stack.sh' from the root of the devstack directory. - - -After finishing the installation, you can find the "Log Management" button on -"Overview" of "Monitoring" tab, if you log in OpenStack as admin. -At first time, you need to specify the index pattern and time-field name. - -The index name is created as the following format. - logs-\[mini-mon tenant id\]-YYYY-MM-DD -For example: - logs-20c4fbd37a2345dd84266dfc92da7bd1-2016-04-07 - -Set the value as the above to index pattern. -Or you can use "\*" as a wild card, like below. - logs-20c4fbd37a2345dd84266dfc92da7bd1-\* - -Select @timestamp as time-field name. - -4. Extra settings - -## Using WSGI - -monasca-log-api can be deployed with Apache using mod_uwsgi. -By default monasca-log-api by default runs under gunicorn. -If you wish to use Apache make sure that ```devstack/local.conf``` -contains: - -```sh -MONASCA_LOG_API_USE_MOD_WSGI=True -``` - -Actual ```MONASCA_LOG_API_DEPLOY``` value is determined using devstack`s -```WSGI_MODE``` variable. Nevertheless there are only three possible values, -that ```MONASCA_LOG_API_DEPLOY``` can take: - -* ```gunicorn``` if ```MONASCA_LOG_API_USE_MOD_WSGI=False``` -* ```mod_wsgi``` if ```MONASCA_LOG_API_USE_MOD_WSGI=True && WSGI_MODE="mod_wsgi""``` -* ```uwsgi``` if ```MONASCA_LOG_API_USE_MOD_WSGI=True && WSGI_MODE="uwsgi""``` - -# Using Vagrant - -Vagrant can be used to deploy a VM with Devstack and Monasca Logging -running in it using the Vagrantfile. After installing Vagrant, -just run the command `vagrant up` as usual in the `monasca-log-api/devstack` -directory. - -``` -# Copyright 2016 FUJITSU LIMITED -# -# 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. -``` diff --git a/devstack/Vagrantfile b/devstack/Vagrantfile deleted file mode 100644 index a4a0d456..00000000 --- a/devstack/Vagrantfile +++ /dev/null @@ -1,123 +0,0 @@ -require 'vagrant.rb' -Vagrant.configure(2) do |config| - - config.cache.scope = :box if Vagrant.has_plugin?("vagrant-cachier") - config.timezone.value = :host if Vagrant.has_plugin?('vagrant-timezone') - if Vagrant.has_plugin?('vagrant-proxyconf') - config.proxy.http = ENV['http_proxy'] if ENV['http_proxy'] - config.proxy.https = ENV['https_proxy'] if ENV['https_proxy'] - if ENV['no_proxy'] - local_no_proxy = ",192.168.10.6,10.0.2.15" - config.proxy.no_proxy = ENV['no_proxy'] + local_no_proxy - end - end - - config.ssh.forward_agent = true - - config.vm.hostname = "devstack" - config.vm.box = "bento/ubuntu-18.04" - config.vm.network "private_network",ip:"192.168.10.6" - config.vm.synced_folder "~/", "/vagrant_home" - config.vm.provider "virtualbox" do |vb| - vb.gui = false - vb.memory = "12800" - vb.cpus = 4 - end - - config.vm.provision "shell", privileged: false, inline: <<-SHELL - sudo apt-get -y install git - - if [ $http_proxy ]; then - git config --global url.https://opendev.org/.insteadOf https://opendev.org/ - sudo git config --global url.https://opendev.org/.insteadOf https://opendev.org/ - - protocol=`echo $http_proxy | awk -F: '{print $1}'` - host=`echo $http_proxy | awk -F/ '{print $3}' | awk -F: '{print $1}'` - port=`echo $http_proxy | awk -F/ '{print $3}' | awk -F: '{print $2}'` - - echo " - - - $host - true - $protocol - $host - $port - - - " > ./maven_proxy_settings.xml - - mkdir ~/.m2 - cp ./maven_proxy_settings.xml ~/.m2/settings.xml - - sudo mkdir /root/.m2 - sudo cp ./maven_proxy_settings.xml /root/.m2/settings.xml - fi - - git clone https://opendev.org/openstack/devstack --branch master --depth 1 - cd devstack - echo '[[local|localrc]] - -GIT_DEPTH=1 -DEST=/opt/stack -USE_VENV=False - -SERVICE_HOST=192.168.10.6 -HOST_IP=192.168.10.6 -DATABASE_HOST=192.168.10.6 -MYSQL_HOST=192.168.10.6 -HOST_IP_IFACE=eth1 - -MYSQL_PASSWORD=secretmysql -DATABASE_PASSWORD=secretdatabase -RABBIT_PASSWORD=secretrabbit -ADMIN_PASSWORD=secretadmin -SERVICE_PASSWORD=secretservice - -LOGFILE=$DEST/logs/stack.sh.log -LOGDIR=$DEST/logs -LOG_COLOR=False - -disable_all_services -enable_service zookeeper rabbit mysql key tempest horizon - -# Enable more OpenStack services if neccessary: -# https://github.com/openstack-dev/devstack/blob/master/stackrc#L56-L81 -# Nova - services to support libvirt based openstack clouds -# enable_service n-api n-cpu n-cond n-sch n-novnc n-cauth n-api-meta -# Placement and Glance services needed for Nova -# enable_service placement-api placement-client -# enable_service g-api g-reg -# Cinder, Neutron -# enable_service cinder c-api c-vol c-sch c-bak -# enable_service neutron q-svc q-agt q-dhcp q-meta q-l3 - -# The following two variables allow switching between Java and Python for the implementations -# of the Monasca API and the Monasca Persister. If these variables are not set, then the -# default is to install the Python implementations of both the Monasca API and the Monasca Persister. - -# Uncomment one of the following two lines to choose Java or Python for the Monasca API. -# MONASCA_API_IMPLEMENTATION_LANG=${MONASCA_API_IMPLEMENTATION_LANG:-java} -MONASCA_API_IMPLEMENTATION_LANG=${MONASCA_API_IMPLEMENTATION_LANG:-python} - -# Uncomment one of the following two lines to choose Java or Python for the Monasca Pesister. -# MONASCA_PERSISTER_IMPLEMENTATION_LANG=${MONASCA_PERSISTER_IMPLEMENTATION_LANG:-java} -MONASCA_PERSISTER_IMPLEMENTATION_LANG=${MONASCA_PERSISTER_IMPLEMENTATION_LANG:-python} - -# Uncomment one of the following two lines to choose either InfluxDB or Vertica. -# MONASCA_METRICS_DB=${MONASCA_METRICS_DB:-vertica} -# MONASCA_METRICS_DB=${MONASCA_METRICS_DB:-cassandra} -MONASCA_METRICS_DB=${MONASCA_METRICS_DB:-influxdb} - -# Uncomment following line to deploy monasca-log-api with Apache -# MONASCA_LOG_API_USE_MOD_WSGI=True - -# Uncomment one of the following lines and modify accordingly to enable the Monasca DevStack Plugin -enable_plugin monasca-api https://opendev.org/openstack/monasca-api.git -enable_plugin monasca-log-api https://opendev.org/openstack/monasca-log-api.git - -' > local.conf - ./stack.sh - SHELL - -end diff --git a/devstack/files/apache-log-api.template b/devstack/files/apache-log-api.template deleted file mode 100644 index 946b42d2..00000000 --- a/devstack/files/apache-log-api.template +++ /dev/null @@ -1,25 +0,0 @@ -Listen %PUBLICPORT% - - - WSGIDaemonProcess monasca-log-api processes=%APIWORKERS% threads=1 user=%USER% display-name=%{GROUP} %VIRTUALENV% - WSGIProcessGroup monasca-log-api - WSGIScriptAlias / %PUBLICWSGI% - WSGIApplicationGroup %{GLOBAL} - WSGIPassAuthorization On - = 2.4> - ErrorLogFormat "%M" - - ErrorLog /var/log/%APACHE_NAME%/monasca-log-api.log - %SSLENGINE% - %SSLCERTFILE% - %SSLKEYFILE% - - -Alias /logs %PUBLICWSGI% - - SetHandler wsgi-script - Options +ExecCGI - WSGIProcessGroup monasca-log-api - WSGIApplicationGroup %{GLOBAL} - WSGIPassAuthorization On - diff --git a/devstack/files/elasticsearch/elasticsearch.yml b/devstack/files/elasticsearch/elasticsearch.yml deleted file mode 100644 index 6d013bb8..00000000 --- a/devstack/files/elasticsearch/elasticsearch.yml +++ /dev/null @@ -1,360 +0,0 @@ -##################### Elasticsearch Configuration Example ##################### - -# This file contains an overview of various configuration settings, -# targeted at operations staff. Application developers should -# consult the guide at . -# -# The installation procedure is covered at -# . -# -# Elasticsearch comes with reasonable defaults for most settings, -# so you can try it out without bothering with configuration. -# -# Most of the time, these defaults are just fine for running a production -# cluster. If you're fine-tuning your cluster, or wondering about the -# effect of certain configuration option, please _do ask_ on the -# mailing list or IRC channel [http://elasticsearch.org/community]. - -# Any element in the configuration can be replaced with environment variables -# by placing them in ${...} notation. For example: -# -#node.rack: ${RACK_ENV_VAR} - -# For information on supported formats and syntax for the config file, see -# - - -################################### Cluster ################################### - -# Cluster name identifies your cluster for auto-discovery. If you're running -# multiple clusters on the same network, make sure you're using unique names. -# -cluster.name: monasca_elastic - - -#################################### Node ##################################### - -# Node names are generated dynamically on startup, so you're relieved -# from configuring them manually. You can tie this node to a specific name: -# -node.name: "devstack" - -# Allow this node to be eligible as a master node (enabled by default): -node.master: true - -# Allow this node to store data (enabled by default) -node.data: true - -# You can exploit these settings to design advanced cluster topologies. -# -# 1. You want this node to never become a master node, only to hold data. -# This will be the "workhorse" of your cluster. -# -#node.master: false -#node.data: true -# -# 2. You want this node to only serve as a master: to not store any data and -# to have free resources. This will be the "coordinator" of your cluster. -# -#node.master: true -#node.data: false -# -# 3. You want this node to be neither master nor data node, but -# to act as a "search load balancer" (fetching data from nodes, -# aggregating results, etc.) -# -#node.master: false -#node.data: false - -# Use the Cluster Health API [http://localhost:9200/_cluster/health], the -# Node Info API [http://localhost:9200/_nodes] or GUI tools -# such as , -# , -# and -# to inspect the cluster state. - -# A node can have generic attributes associated with it, which can later be used -# for customized shard allocation filtering, or allocation awareness. An attribute -# is a simple key value pair, similar to node.key: value, here is an example: -# -#node.rack: rack314 - -# By default, multiple nodes are allowed to start from the same installation location -# to disable it, set the following: -#node.max_local_storage_nodes: 1 - - -#################################### Index #################################### - -# You can set a number of options (such as shard/replica options, mapping -# or analyzer definitions, translog settings, ...) for indices globally, -# in this file. -# -# Note, that it makes more sense to configure index settings specifically for -# a certain index, either when creating it or by using the index templates API. -# -# See and -# -# for more information. - -# Set the number of shards (splits) of an index (5 by default): -# -#index.number_of_shards: 5 - -# Set the number of replicas (additional copies) of an index (1 by default): -# -#index.number_of_replicas: 1 - -# Note, that for development on a local machine, with small indices, it usually -# makes sense to "disable" the distributed features: -# -#index.number_of_shards: 1 -#index.number_of_replicas: 0 - -# These settings directly affect the performance of index and search operations -# in your cluster. Assuming you have enough machines to hold shards and -# replicas, the rule of thumb is: -# -# 1. Having more *shards* enhances the _indexing_ performance and allows to -# _distribute_ a big index across machines. -# 2. Having more *replicas* enhances the _search_ performance and improves the -# cluster _availability_. -# -# The "number_of_shards" is a one-time setting for an index. -# -# The "number_of_replicas" can be increased or decreased anytime, -# by using the Index Update Settings API. -# -# Elasticsearch takes care about load balancing, relocating, gathering the -# results from nodes, etc. Experiment with different settings to fine-tune -# your setup. - -# Use the Index Status API () to inspect -# the index status. - - -#################################### Paths #################################### - -# Path to directory where to store index data allocated for this node. -path.data: %ES_DATA_DIR% - -# Path to log files: -path.logs: %ES_LOG_DIR% - -# Path to where plugins are installed: -#path.plugins: /path/to/plugins - -# Path to temporary files -#path.work: /path/to/work - -# Path to directory containing configuration (this file and logging.yml): -#path.conf: /path/to/conf - - -#################################### Plugin ################################### - -# If a plugin listed here is not installed for current node, the node will not start. -# -#plugin.mandatory: mapper-attachments,lang-groovy - - -################################### Memory #################################### - -# Elasticsearch performs poorly when JVM starts swapping: you should ensure that -# it _never_ swaps. -# -# Set this property to true to lock the memory: -# -#bootstrap.mlockall: true - -# Make sure that the ES_MIN_MEM and ES_MAX_MEM environment variables are set -# to the same value, and that the machine has enough memory to allocate -# for Elasticsearch, leaving enough memory for the operating system itself. -# -# You should also make sure that the Elasticsearch process is allowed to lock -# the memory, eg. by using `ulimit -l unlimited`. - - -############################## Network And HTTP ############################### - -# Elasticsearch, by default, binds itself to the 0.0.0.0 address, and listens -# on port [9200-9300] for HTTP traffic and on port [9300-9400] for node-to-node -# communication. (the range means that if the port is busy, it will automatically -# try the next port). - -# Set the bind address specifically (IPv4 or IPv6): -network.bind_host: %ES_SERVICE_BIND_HOST% - -# Set the address other nodes will use to communicate with this node. If not -# set, it is automatically derived. It must point to an actual IP address. -network.publish_host: %ES_SERVICE_PUBLISH_HOST% - -# Set a custom port for the node to node communication (9300 by default): -transport.tcp.port: %ES_SERVICE_PUBLISH_PORT% - -# Enable compression for all communication between nodes (disabled by default): -# -#transport.tcp.compress: true - -# Set a custom port to listen for HTTP traffic: -# -http.port: %ES_SERVICE_BIND_PORT% - -# Set a custom allowed content length: -# -#http.max_content_length: 100mb - -# Disable HTTP completely: -# -#http.enabled: false - - -################################### Gateway ################################### - -# The gateway allows for persisting the cluster state between full cluster -# restarts. Every change to the state (such as adding an index) will be stored -# in the gateway, and when the cluster starts up for the first time, -# it will read its state from the gateway. - -# There are several types of gateway implementations. For more information, see -# . - -# The default gateway type is the "local" gateway (recommended): -# -#gateway.type: local - -# Settings below control how and when to start the initial recovery process on -# a full cluster restart (to reuse as much local data as possible when using shared -# gateway). - -# Allow recovery process after N nodes in a cluster are up: -# -#gateway.recover_after_nodes: 1 -# Set the timeout to initiate the recovery process, once the N nodes -# from previous setting are up (accepts time value): -# -#gateway.recover_after_time: 5m - -# Set how many nodes are expected in this cluster. Once these N nodes -# are up (and recover_after_nodes is met), begin recovery process immediately -# (without waiting for recover_after_time to expire): -# -#gateway.expected_nodes: 2 - - -############################# Recovery Throttling ############################# - -# These settings allow to control the process of shards allocation between -# nodes during initial recovery, replica allocation, rebalancing, -# or when adding and removing nodes. - -# Set the number of concurrent recoveries happening on a node: -# -# 1. During the initial recovery -# -#cluster.routing.allocation.node_initial_primaries_recoveries: 4 -# -# 2. During adding/removing nodes, rebalancing, etc -# -#cluster.routing.allocation.node_concurrent_recoveries: 2 - -# Set to throttle throughput when recovering (eg. 100mb, by default 20mb): -# -#indices.recovery.max_bytes_per_sec: 20mb - -# Set to limit the number of open concurrent streams when -# recovering a shard from a peer: -# -#indices.recovery.concurrent_streams: 5 - - -################################## Discovery ################################## - -# Discovery infrastructure ensures nodes can be found within a cluster -# and master node is elected. Multicast discovery is the default. - -# Set to ensure a node sees N other master eligible nodes to be considered -# operational within the cluster. This should be set to a quorum/majority of -# the master-eligible nodes in the cluster. -# -discovery.zen.minimum_master_nodes: 1 - -# Set the time to wait for ping responses from other nodes when discovering. -# Set this option to a higher value on a slow or congested network -# to minimize discovery failures: -# -#discovery.zen.ping.timeout: 3s - -# For more information, see -# - -# Unicast discovery allows to explicitly control which nodes will be used -# to discover the cluster. It can be used when multicast is not present, -# or to restrict the cluster communication-wise. -# -# 1. Disable multicast discovery (enabled by default): -# -discovery.zen.ping.multicast.enabled: false -# 2. Configure an initial list of master nodes in the cluster -# to perform discovery when new nodes (master or data) are started: -# -# discovery.zen.ping.unicast.hosts: [127.0.0.1] - -# EC2 discovery allows to use AWS EC2 API in order to perform discovery. -# -# You have to install the cloud-aws plugin for enabling the EC2 discovery. -# -# For more information, see -# -# -# See -# for a step-by-step tutorial. - -# GCE discovery allows to use Google Compute Engine API in order to perform discovery. -# -# You have to install the cloud-gce plugin for enabling the GCE discovery. -# -# For more information, see . - -# Azure discovery allows to use Azure API in order to perform discovery. -# -# You have to install the cloud-azure plugin for enabling the Azure discovery. -# -# For more information, see . - -################################## Slow Log ################################## - -# Shard level query and fetch threshold logging. - -#index.search.slowlog.threshold.query.warn: 10s -#index.search.slowlog.threshold.query.info: 5s -#index.search.slowlog.threshold.query.debug: 2s -#index.search.slowlog.threshold.query.trace: 500ms - -#index.search.slowlog.threshold.fetch.warn: 1s -#index.search.slowlog.threshold.fetch.info: 800ms -#index.search.slowlog.threshold.fetch.debug: 500ms -#index.search.slowlog.threshold.fetch.trace: 200ms - -#index.indexing.slowlog.threshold.index.warn: 10s -#index.indexing.slowlog.threshold.index.info: 5s -#index.indexing.slowlog.threshold.index.debug: 2s -#index.indexing.slowlog.threshold.index.trace: 500ms - -################################## GC Logging ################################ - -#monitor.jvm.gc.young.warn: 1000ms -#monitor.jvm.gc.young.info: 700ms -#monitor.jvm.gc.young.debug: 400ms - -#monitor.jvm.gc.old.warn: 10s -#monitor.jvm.gc.old.info: 5s -#monitor.jvm.gc.old.debug: 2s - -################################## Security ################################ - -# Uncomment if you want to enable JSONP as a valid return transport on the -# http server. With this enabled, it may pose a security risk, so disabling -# it unless you need it is recommended (it is disabled by default). -# -#http.jsonp.enable: true diff --git a/devstack/files/grafana/dashboards.d/09-monasca.json b/devstack/files/grafana/dashboards.d/09-monasca.json deleted file mode 100644 index 84935135..00000000 --- a/devstack/files/grafana/dashboards.d/09-monasca.json +++ /dev/null @@ -1,2138 +0,0 @@ -{ - "id": null, - "title": "Monasca Monitoring", - "originalTitle": "Monasca Monitoring", - "tags": [], - "style": "dark", - "timezone": "utc", - "editable": true, - "hideControls": false, - "sharedCrosshair": false, - "rows": [ - { - "title": "Monasca Health - metrics", - "height": "100px", - "editable": true, - "collapse": false, - "panels": [ - { - "title": "Metrics API", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 19, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "uwsgi"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - }, - { - "value": "9", - "op": "=", - "text": "UP" - }, - { - "value": "10", - "op": "=", - "text": "UP" - }, - { - "value": "11", - "op": "=", - "text": "UP" - }, - { - "value": "12", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "0.0,0.2,1.0", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "Storm", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 44, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "storm"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "Persister", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 21, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "persister"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "Metrics DB", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 51, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "http_status", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "influxdb"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "0", - "op": "=", - "text": "UP" - }, - { - "value": "1", - "op": "=", - "text": "DOWN" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(71, 212, 59, 0.4)", - "rgba(245, 150, 40, 0.73)", - "rgba(225, 40, 40, 0.59)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "Notification Engine", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 42, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "monasca-notification"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - } - ], - "showTitle": true - }, - { - "title": "Monasca Health - Logs", - "height": "100px", - "editable": true, - "collapse": false, - "panels": [ - { - "title": "Log API", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 20, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "log-api"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - }, - { - "value": "9", - "op": "=", - "text": "UP" - }, - { - "value": "10", - "op": "=", - "text": "UP" - }, - { - "value": "11", - "op": "=", - "text": "UP" - }, - { - "value": "12", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "Log Transformer", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 54, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "log-transformer"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "Log Metrics", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 102, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "log-metrics"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "Log Persister", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 53, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "log-persister"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "Log DB", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 33, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "elasticsearch.cluster_status", - "condition_filter": true - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - }, - { - "value": "9", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "Kibana", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 22, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "http_status", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "kibana"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "0", - "op": "=", - "text": "UP" - }, - { - "value": "1", - "op": "=", - "text": "DOWN" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(71, 212, 59, 0.4)", - "rgba(245, 150, 40, 0.73)", - "rgba(225, 40, 40, 0.59)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - } - ], - "showTitle": true - }, - { - "title": "Monasca Health - Common", - "height": "100px", - "editable": true, - "collapse": false, - "panels": [ - { - "title": "Kafka", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 38, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "kafka"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "ZooKeeper", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 48, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "zookeeper"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "MariaDB", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 66, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "mysqld"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - }, - { - "title": "Statsd", - "error": false, - "span": 2, - "editable": true, - "type": "singlestat", - "id": 166, - "links": [], - "maxDataPoints": 100, - "interval": null, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "monasca-statsd"}] - } - ], - "cacheTimeout": null, - "format": "none", - "prefix": "", - "postfix": "", - "nullText": null, - "valueMaps": [ - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - } - ], - "nullPointMode": "connected", - "valueName": "current", - "prefixFontSize": "50%", - "valueFontSize": "80%", - "postfixFontSize": "50%", - "thresholds": "-1.0,0.2,0.8", - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "sparkline": { - "show": false, - "full": false, - "lineColor": "rgb(31, 120, 193)", - "fillColor": "rgba(31, 118, 189, 0.18)" - } - } - ], - "showTitle": true - }, - { - "title": "System resources", - "height": "250px", - "editable": true, - "collapse": false, - "panels": [ - { - "title": "CPU usage", - "error": false, - "span": 6, - "editable": true, - "type": "graph", - "id": 23, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "y_formats": [ - "percent", - "short" - ], - "grid": { - "leftMax": 100, - "rightMax": null, - "leftMin": 0, - "rightMin": null, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false, - "alignAsTable": true - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": false - }, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "cpu.percent", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring" - }, - { - "target": "", - "aggregator": "none", - "column": "value", - "metric": "cpu.user_perc", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring" - }, - { - "target": "", - "aggregator": "none", - "column": "value", - "metric": "cpu.system_perc", - "condition_filter": true - }, - { - "target": "", - "aggregator": "none", - "column": "value", - "metric": "cpu.wait_perc", - "condition_filter": true - } - ], - "aliasColors": {}, - "seriesOverrides": [], - "links": [] - }, - { - "title": "Memory usage", - "error": false, - "span": 6, - "editable": true, - "type": "graph", - "id": 24, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "y_formats": [ - "none", - "short" - ], - "grid": { - "leftMax": null, - "rightMax": null, - "leftMin": 0, - "rightMin": null, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false, - "alignAsTable": true - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "individual", - "shared": true - }, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "mem.total_mb", - "condition_filter": true - }, - { - "target": "", - "aggregator": "none", - "column": "value", - "metric": "mem.used_mb", - "period": "", - "condition_filter": true - }, - { - "target": "", - "aggregator": "none", - "column": "value", - "metric": "mem.swap_total_mb", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring" - }, - { - "target": "", - "aggregator": "none", - "column": "value", - "metric": "mem.swap_used_mb", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring" - }, - { - "target": "", - "aggregator": "none", - "column": "value", - "metric": "mem.used_cache", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring" - } - ], - "aliasColors": {}, - "seriesOverrides": [], - "links": [], - "leftYAxisLabel": "MB" - }, - { - "title": "Disk usage", - "error": false, - "span": 6, - "editable": true, - "type": "graph", - "id": 25, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "y_formats": [ - "percent", - "short" - ], - "grid": { - "leftMax": 100, - "rightMax": null, - "leftMin": 0, - "rightMin": null, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false, - "alignAsTable": true - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true - }, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "disk.space_used_perc", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring", - "dimensions": [ - { - "key": "mount_point", - "value": "/rootfs" - } - ] - } - ], - "aliasColors": {}, - "seriesOverrides": [], - "links": [] - }, - { - "title": "System load", - "error": false, - "span": 6, - "editable": true, - "type": "graph", - "id": 26, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "y_formats": [ - "none", - "short" - ], - "grid": { - "leftMax": null, - "rightMax": null, - "leftMin": 0, - "rightMin": null, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false, - "alignAsTable": true - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true - }, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "load.avg_1_min", - "period": "", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring" - }, - { - "aggregator": "none", - "column": "value", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring", - "metric": "load.avg_5_min", - "period": "300", - "refId": "B", - "dimensions": [], - "error": "" - }, - { - "aggregator": "none", - "column": "value", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring", - "metric": "load.avg_15_min", - "period": "300", - "refId": "C", - "dimensions": [], - "error": "" - } - ], - "aliasColors": {}, - "seriesOverrides": [], - "links": [] - } - ], - "showTitle": true - }, - { - "title": "Network Monitoring", - "height": "250px", - "editable": true, - "collapse": false, - "panels": [ - { - "title": "Network usage", - "error": false, - "span": 6, - "editable": true, - "type": "graph", - "id": 61, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "y_formats": [ - "bps", - "short" - ], - "grid": { - "leftMax": null, - "rightMax": null, - "leftMin": 0, - "rightMin": null, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false, - "alignAsTable": true - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true - }, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "net.in_bytes_sec", - "merge": true, - "dimensions": "", - "period": "", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring" - }, - { - "target": "", - "aggregator": "none", - "column": "value", - "metric": "net.out_bytes_sec", - "merge": true, - "dimensions": "", - "period": "", - "condition_filter": true, - "condition_key": "service", - "condition_value": "monitoring" - } - ], - "aliasColors": {}, - "seriesOverrides": [], - "links": [] - } - ], - "showTitle": true - }, - { - "title": "Kafka", - "height": "250px", - "editable": true, - "collapse": false, - "panels": [ - { - "title": "CPU usage", - "error": false, - "span": 4, - "editable": true, - "type": "graph", - "id": 60, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "y_formats": [ - "percent", - "short" - ], - "grid": { - "leftMax": null, - "rightMax": null, - "leftMin": 0, - "rightMin": null, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false, - "alignAsTable": true - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true - }, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.cpu_perc", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "kafka"}] - } - ], - "aliasColors": {}, - "seriesOverrides": [], - "links": [] - }, - { - "title": "Allocated memory", - "error": false, - "span": 4, - "editable": true, - "type": "graph", - "id": 59, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "y_formats": [ - "none", - "short" - ], - "grid": { - "leftMax": null, - "rightMax": null, - "leftMin": 0, - "rightMin": null, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false, - "alignAsTable": true - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true - }, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.mem.rss_mbytes", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "kafka"}] - } - ], - "aliasColors": {}, - "seriesOverrides": [], - "leftYAxisLabel": "MB", - "links": [] - }, - { - "title": "PID count", - "error": false, - "span": 4, - "editable": true, - "type": "graph", - "id": 89, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "y_formats": [ - "none", - "short" - ], - "grid": { - "leftMax": null, - "rightMax": null, - "leftMin": 0, - "rightMin": null, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false, - "alignAsTable": true - }, - "nullPointMode": "connected", - "steppedLine": true, - "tooltip": { - "value_type": "cumulative", - "shared": true - }, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "process.pid_count", - "condition_filter": true, - "dimensions": [{"key": "service", "value": "kafka"}] - } - ], - "aliasColors": {}, - "seriesOverrides": [], - "links": [] - }, - { - "title": "Consumer lag", - "error": false, - "span": 12, - "editable": true, - "type": "graph", - "id": 58, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "y_formats": [ - "none", - "short" - ], - "grid": { - "leftMax": null, - "rightMax": null, - "leftMin": 0, - "rightMin": null, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false, - "alignAsTable": true - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true - }, - "targets": [ - { - "aggregator": "none", - "column": "value", - "metric": "kafka.consumer_lag", - "refId": "A", - "period": "300", - "dimensions": [ - { - "key": "consumer_group", - "value": "$all" - } - ], - "error": "" - } - ], - "aliasColors": {}, - "seriesOverrides": [], - "links": [] - } - ], - "showTitle": true - }, - { - "title": "Components", - "panels": [ - { - "id": 167, - "title": "Common", - "span": 4, - "type": "dashlist", - "query": "", - "limit": 10, - "tags": [ - "common" - ], - "recent": false, - "search": true, - "starred": false, - "headings": false, - "links": [] - }, - { - "id": 168, - "title": "Metrics", - "span": 4, - "type": "dashlist", - "query": "", - "limit": 10, - "tags": [ - "metrics" - ], - "recent": false, - "search": true, - "starred": false, - "headings": false, - "links": [] - }, - { - "headings": false, - "id": 169, - "limit": 10, - "links": [], - "query": "", - "recent": false, - "search": true, - "span": 4, - "starred": false, - "tags": [ - "logs" - ], - "title": "Logs", - "type": "dashlist" - } - ] - } - ], - "time": { - "from": "now-1h", - "to": "now" - }, - "templating": { - "list": [], - "enable": false - }, - "annotations": { - "enable": false, - "list": [] - }, - "refresh": "30s", - "version": 6, - "hideAllLegends": false -} diff --git a/devstack/files/grafana/dashboards.d/20-kibana.json b/devstack/files/grafana/dashboards.d/20-kibana.json deleted file mode 100644 index 712afb00..00000000 --- a/devstack/files/grafana/dashboards.d/20-kibana.json +++ /dev/null @@ -1,624 +0,0 @@ -{ - "id": null, - "title": "Kibana", - "tags": [ - "logs" - ], - "style": "dark", - "timezone": "browser", - "editable": true, - "sharedCrosshair": false, - "hideControls": false, - "time": { - "from": "now-1h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 7, - "links": [], - "gnetId": null, - "rows": [ - { - "title": "Dashboard Row", - "panels": [ - { - "cacheTimeout": null, - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "id": 1, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "span": 4, - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "targets": [ - { - "aggregator": "none", - "dimensions": [ - { - "key": "service", - "value": "kibana" - } - ], - "error": "", - "metric": "process.pid_count", - "period": "300", - "refId": "A" - } - ], - "thresholds": "0.2,0.8", - "title": "Kibana", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - }, - { - "value": "9", - "op": "=", - "text": "UP" - }, - { - "value": "10", - "op": "=", - "text": "UP" - }, - { - "value": "11", - "op": "=", - "text": "UP" - }, - { - "value": "12", - "op": "=", - "text": "UP" - } - ], - "valueName": "current" - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 2, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "kibana" - } - ], - "error": "", - "metric": "process.cpu_perc", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "CPU", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "percent", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 3, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "kibana" - } - ], - "error": "", - "metric": "process.mem.rss_mbytes", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Memory", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": "250px", - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - }, - { - "title": "Dashboard Row", - "panels": [ - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 4, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "kibana" - } - ], - "error": "", - "metric": "process.io.read_count", - "period": "300", - "refId": "A" - }, - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "kibana" - } - ], - "error": "", - "metric": "process.io.write_count", - "period": "300", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "IO Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 5, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "kibana" - } - ], - "error": "", - "metric": "process.io.read_kbytes", - "period": "300", - "refId": "A" - }, - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "kibana" - } - ], - "error": "", - "metric": "process.io.write_kbytes", - "period": "300", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "IO Read/Write [kB]", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "kbytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": 250, - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - }, - { - "title": "Dashboard Row", - "panels": [ - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 6, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 12, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "kibana" - } - ], - "error": "", - "metric": "process.open_file_descriptors", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Open File Descriptors", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": 250, - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - } - ] -} diff --git a/devstack/files/grafana/dashboards.d/21-logapi.json b/devstack/files/grafana/dashboards.d/21-logapi.json deleted file mode 100644 index 7d6e15ef..00000000 --- a/devstack/files/grafana/dashboards.d/21-logapi.json +++ /dev/null @@ -1,624 +0,0 @@ -{ - "id": null, - "title": "Log API", - "tags": [ - "logs" - ], - "style": "dark", - "timezone": "browser", - "editable": true, - "sharedCrosshair": false, - "hideControls": false, - "time": { - "from": "now-1h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 7, - "links": [], - "gnetId": null, - "rows": [ - { - "title": "Dashboard Row", - "panels": [ - { - "cacheTimeout": null, - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "id": 1, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "span": 4, - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "targets": [ - { - "aggregator": "none", - "dimensions": [ - { - "key": "service", - "value": "log-api" - } - ], - "error": "", - "metric": "process.pid_count", - "period": "300", - "refId": "A" - } - ], - "thresholds": "0.2,0.8", - "title": "Log API", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - }, - { - "value": "9", - "op": "=", - "text": "UP" - }, - { - "value": "10", - "op": "=", - "text": "UP" - }, - { - "value": "11", - "op": "=", - "text": "UP" - }, - { - "value": "12", - "op": "=", - "text": "UP" - } - ], - "valueName": "current" - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 2, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-api" - } - ], - "error": "", - "metric": "process.cpu_perc", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "CPU", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "percent", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 3, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-api" - } - ], - "error": "", - "metric": "process.mem.rss_mbytes", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Memory", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": "250px", - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - }, - { - "title": "Dashboard Row", - "panels": [ - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 4, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-api" - } - ], - "error": "", - "metric": "process.io.read_count", - "period": "300", - "refId": "A" - }, - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-api" - } - ], - "error": "", - "metric": "process.io.write_count", - "period": "300", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "IO Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 5, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-api" - } - ], - "error": "", - "metric": "process.io.read_kbytes", - "period": "300", - "refId": "A" - }, - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-api" - } - ], - "error": "", - "metric": "process.io.write_kbytes", - "period": "300", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "IO Read/Write [kB]", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "kbytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": 250, - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - }, - { - "title": "Dashboard Row", - "panels": [ - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 6, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 12, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-api" - } - ], - "error": "", - "metric": "process.open_file_descriptors", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Open File Descriptors", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": 250, - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - } - ] -} diff --git a/devstack/files/grafana/dashboards.d/22-logtransformer.json b/devstack/files/grafana/dashboards.d/22-logtransformer.json deleted file mode 100644 index dd9dae5c..00000000 --- a/devstack/files/grafana/dashboards.d/22-logtransformer.json +++ /dev/null @@ -1,624 +0,0 @@ -{ - "id": null, - "title": "Log Transformer", - "tags": [ - "logs" - ], - "style": "dark", - "timezone": "browser", - "editable": true, - "sharedCrosshair": false, - "hideControls": false, - "time": { - "from": "now-1h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 7, - "links": [], - "gnetId": null, - "rows": [ - { - "title": "Dashboard Row", - "panels": [ - { - "cacheTimeout": null, - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "id": 1, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "span": 4, - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "targets": [ - { - "aggregator": "none", - "dimensions": [ - { - "key": "service", - "value": "log-transformer" - } - ], - "error": "", - "metric": "process.pid_count", - "period": "300", - "refId": "A" - } - ], - "thresholds": "0.2,0.8", - "title": "Log Transformer", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - }, - { - "value": "9", - "op": "=", - "text": "UP" - }, - { - "value": "10", - "op": "=", - "text": "UP" - }, - { - "value": "11", - "op": "=", - "text": "UP" - }, - { - "value": "12", - "op": "=", - "text": "UP" - } - ], - "valueName": "current" - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 2, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-transformer" - } - ], - "error": "", - "metric": "process.cpu_perc", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "CPU", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "percent", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 3, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-transformer" - } - ], - "error": "", - "metric": "process.mem.rss_mbytes", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Memory", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": "250px", - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - }, - { - "title": "Dashboard Row", - "panels": [ - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 4, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-transformer" - } - ], - "error": "", - "metric": "process.io.read_count", - "period": "300", - "refId": "A" - }, - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-transformer" - } - ], - "error": "", - "metric": "process.io.write_count", - "period": "300", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "IO Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 5, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-transformer" - } - ], - "error": "", - "metric": "process.io.read_kbytes", - "period": "300", - "refId": "A" - }, - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-transformer" - } - ], - "error": "", - "metric": "process.io.write_kbytes", - "period": "300", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "IO Read/Write [kB]", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "kbytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": 250, - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - }, - { - "title": "Dashboard Row", - "panels": [ - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 6, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 12, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-transformer" - } - ], - "error": "", - "metric": "process.open_file_descriptors", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Open File Descriptors", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": 250, - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - } - ] -} diff --git a/devstack/files/grafana/dashboards.d/23-logtmetrics.json b/devstack/files/grafana/dashboards.d/23-logtmetrics.json deleted file mode 100644 index eb44a73d..00000000 --- a/devstack/files/grafana/dashboards.d/23-logtmetrics.json +++ /dev/null @@ -1,624 +0,0 @@ -{ - "id": null, - "title": "Log Metrics", - "tags": [ - "logs" - ], - "style": "dark", - "timezone": "browser", - "editable": true, - "sharedCrosshair": false, - "hideControls": false, - "time": { - "from": "now-1h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 7, - "links": [], - "gnetId": null, - "rows": [ - { - "title": "Dashboard Row", - "panels": [ - { - "cacheTimeout": null, - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "id": 1, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "span": 4, - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "targets": [ - { - "aggregator": "none", - "dimensions": [ - { - "key": "service", - "value": "log-metrics" - } - ], - "error": "", - "metric": "process.pid_count", - "period": "300", - "refId": "A" - } - ], - "thresholds": "0.2,0.8", - "title": "Log Metrics", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - }, - { - "value": "9", - "op": "=", - "text": "UP" - }, - { - "value": "10", - "op": "=", - "text": "UP" - }, - { - "value": "11", - "op": "=", - "text": "UP" - }, - { - "value": "12", - "op": "=", - "text": "UP" - } - ], - "valueName": "current" - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 2, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-metrics" - } - ], - "error": "", - "metric": "process.cpu_perc", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "CPU", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "percent", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 3, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-metrics" - } - ], - "error": "", - "metric": "process.mem.rss_mbytes", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Memory", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": "250px", - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - }, - { - "title": "Dashboard Row", - "panels": [ - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 4, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-metrics" - } - ], - "error": "", - "metric": "process.io.read_count", - "period": "300", - "refId": "A" - }, - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-metrics" - } - ], - "error": "", - "metric": "process.io.write_count", - "period": "300", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "IO Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 5, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-metrics" - } - ], - "error": "", - "metric": "process.io.read_kbytes", - "period": "300", - "refId": "A" - }, - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-metrics" - } - ], - "error": "", - "metric": "process.io.write_kbytes", - "period": "300", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "IO Read/Write [kB]", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "kbytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": 250, - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - }, - { - "title": "Dashboard Row", - "panels": [ - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 6, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 12, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-metrics" - } - ], - "error": "", - "metric": "process.open_file_descriptors", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Open File Descriptors", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": 250, - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - } - ] -} diff --git a/devstack/files/grafana/dashboards.d/24-logpersister.json b/devstack/files/grafana/dashboards.d/24-logpersister.json deleted file mode 100644 index 28a1adec..00000000 --- a/devstack/files/grafana/dashboards.d/24-logpersister.json +++ /dev/null @@ -1,624 +0,0 @@ -{ - "id": null, - "title": "Log Persister", - "tags": [ - "logs" - ], - "style": "dark", - "timezone": "browser", - "editable": true, - "sharedCrosshair": false, - "hideControls": false, - "time": { - "from": "now-1h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 7, - "links": [], - "gnetId": null, - "rows": [ - { - "title": "Dashboard Row", - "panels": [ - { - "cacheTimeout": null, - "colorBackground": true, - "colorValue": false, - "colors": [ - "rgba(225, 40, 40, 0.59)", - "rgba(245, 150, 40, 0.73)", - "rgba(71, 212, 59, 0.4)" - ], - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "id": 1, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "span": 4, - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "targets": [ - { - "aggregator": "none", - "dimensions": [ - { - "key": "service", - "value": "log-persister" - } - ], - "error": "", - "metric": "process.pid_count", - "period": "300", - "refId": "A" - } - ], - "thresholds": "0.2,0.8", - "title": "Log Persister", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "value": "0", - "op": "=", - "text": "DOWN" - }, - { - "value": "1", - "op": "=", - "text": "UP" - }, - { - "value": "2", - "op": "=", - "text": "UP" - }, - { - "value": "3", - "op": "=", - "text": "UP" - }, - { - "value": "4", - "op": "=", - "text": "UP" - }, - { - "value": "5", - "op": "=", - "text": "UP" - }, - { - "value": "6", - "op": "=", - "text": "UP" - }, - { - "value": "7", - "op": "=", - "text": "UP" - }, - { - "value": "8", - "op": "=", - "text": "UP" - }, - { - "value": "9", - "op": "=", - "text": "UP" - }, - { - "value": "10", - "op": "=", - "text": "UP" - }, - { - "value": "11", - "op": "=", - "text": "UP" - }, - { - "value": "12", - "op": "=", - "text": "UP" - } - ], - "valueName": "current" - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 2, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-persister" - } - ], - "error": "", - "metric": "process.cpu_perc", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "CPU", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "percent", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 3, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-persister" - } - ], - "error": "", - "metric": "process.mem.rss_mbytes", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Memory", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": "250px", - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - }, - { - "title": "Dashboard Row", - "panels": [ - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 4, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-persister" - } - ], - "error": "", - "metric": "process.io.read_count", - "period": "300", - "refId": "A" - }, - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-persister" - } - ], - "error": "", - "metric": "process.io.write_count", - "period": "300", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "IO Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 5, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-persister" - } - ], - "error": "", - "metric": "process.io.read_kbytes", - "period": "300", - "refId": "A" - }, - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-persister" - } - ], - "error": "", - "metric": "process.io.write_kbytes", - "period": "300", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "IO Read/Write [kB]", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "kbytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": 250, - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - }, - { - "title": "Dashboard Row", - "panels": [ - { - "aliasColors": {}, - "bars": false, - "datasource": null, - "fill": 1, - "id": 6, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 12, - "stack": false, - "steppedLine": false, - "targets": [ - { - "aggregator": "avg", - "dimensions": [ - { - "key": "process_name", - "value": "log-persister" - } - ], - "error": "", - "metric": "process.open_file_descriptors", - "period": "300", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Open File Descriptors", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "showTitle": false, - "titleSize": "h6", - "height": 250, - "repeat": null, - "repeatRowId": null, - "repeatIteration": null, - "collapse": false - } - ] -} diff --git a/devstack/files/grafana/grafana.py b/devstack/files/grafana/grafana.py deleted file mode 100644 index c78d119d..00000000 --- a/devstack/files/grafana/grafana.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 - -# (C) Copyright 2017 Hewlett Packard Enterprise Development LP -# (C) Copyright 2018 FUJITSU LIMITED -# -# 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 glob -import json -import logging -import os -import sys -import time - -from requests import RequestException -from requests import Session - -LOG_LEVEL = logging.getLevelName(os.environ.get('LOG_LEVEL', 'INFO')) -logging.basicConfig(level=LOG_LEVEL) - -logger = logging.getLogger(__name__) - -GRAFANA_URL = os.environ.get('GRAFANA_URL', 'http://localhost:3000') -GRAFANA_USERNAME = os.environ.get('GRAFANA_USERNAME', 'mini-mon') -GRAFANA_PASSWORD = os.environ.get('GRAFANA_PASSWORD', 'password') -GRAFANA_USERS = [{'user': GRAFANA_USERNAME, 'password': GRAFANA_PASSWORD, 'email': ''}] - -DASHBOARDS_DIR = sys.argv[1] - - -def retry(retries=5, delay=2.0, exc_types=(RequestException,)): - def decorator(func): - def f_retry(*args, **kwargs): - for i in range(retries): - try: - return func(*args, **kwargs) - except exc_types as exc: - if i < retries - 1: - logger.debug('Caught exception, retrying...', - exc_info=True) - time.sleep(delay) - else: - logger.exception('Failed after %d attempts', retries) - if isinstance(exc, RequestException): - logger.debug('Response was: %r', exc.response.text) - - raise - return f_retry - return decorator - - -def create_login_payload(): - if os.environ.get('GRAFANA_USERS'): - try: - json.loads(os.environ.get('GRAFANA_USERS')) - except ValueError: - print("Invalid type GRAFANA_USERS") - raise - grafana_users = json.loads(os.environ.get('GRAFANA_USERS')) - else: - grafana_users = GRAFANA_USERS - return grafana_users - - -@retry(retries=24, delay=5.0) -def login(session, user): - r = session.post('{url}/login'.format(url=GRAFANA_URL), - json=user, - timeout=5) - r.raise_for_status() - - -def create_dashboard_payload(json_path): - with open(json_path, 'r') as f: - dashboard = json.load(f) - dashboard['id'] = None - - return { - 'dashboard': dashboard, - 'overwrite': True - } - - -def main(): - for user in create_login_payload(): - logging.info('Opening a Grafana session...') - session = Session() - login(session, user) - - for path in sorted(glob.glob('{dir}/*.json'.format(dir=DASHBOARDS_DIR))): - logging.info('Creating dashboard from file: {path}'.format(path=path)) - r = session.post('{url}/api/dashboards/db'.format(url=GRAFANA_URL), - json=create_dashboard_payload(path)) - logging.debug('Response: %r', r.json()) - r.raise_for_status() - - logging.info('Ending %r session...', user.get('user')) - session.get('{url}/logout'.format(url=GRAFANA_URL)) - - logging.info('Finished successfully.') - - -if __name__ == '__main__': - main() diff --git a/devstack/files/kibana/kibana.yml b/devstack/files/kibana/kibana.yml deleted file mode 100644 index 0f7b02b4..00000000 --- a/devstack/files/kibana/kibana.yml +++ /dev/null @@ -1,78 +0,0 @@ -# Kibana is served by a back end server. This controls which port to use. -server.port: %KIBANA_SERVICE_PORT% - -# The host to bind the server to. -server.host: %KIBANA_SERVICE_HOST% - -# If you are running kibana behind a proxy, and want to mount it at a path, -# specify that path here. The basePath can't end in a slash. -server.basePath: /dashboard/monitoring/logs_proxy - -# The Elasticsearch instance to use for all your queries. -elasticsearch.url: http://%ES_SERVICE_BIND_HOST%:%ES_SERVICE_BIND_PORT% - -# preserve_elasticsearch_host true will send the hostname specified in `elasticsearch`. If you set it to false, -# then the host you use to connect to *this* Kibana instance will be sent. -elasticsearch.preserveHost: True - -# Kibana uses an index in Elasticsearch to store saved searches, visualizations -# and dashboards. It will create a new index if it doesn't already exist. -kibana.index: ".kibana" - -# The default application to load. -kibana.defaultAppId: "discover" - -# If your Elasticsearch is protected with basic auth, these are the user credentials -# used by the Kibana server to perform maintenance on the kibana_index at startup. Your Kibana -# users will still need to authenticate with Elasticsearch (which is proxied through -# the Kibana server) -# elasticsearch.username: "user" -# elasticsearch.password: "pass" - -# SSL for outgoing requests from the Kibana Server to the browser (PEM formatted) -# server.ssl.cert: /path/to/your/server.crt -# server.ssl.key: /path/to/your/server.key - -# Optional setting to validate that your Elasticsearch backend uses the same key files (PEM formatted) -# elasticsearch.ssl.cert: /path/to/your/client.crt -# elasticsearch.ssl.key: /path/to/your/client.key - -# If you need to provide a CA certificate for your Elasticsearch instance, put -# the path of the pem file here. -# elasticsearch.ssl.ca: /path/to/your/CA.pem - -# Set to false to have a complete disregard for the validity of the SSL -# certificate. -# elasticsearch.ssl.verify: true - -# Time in milliseconds to wait for elasticsearch to respond to pings, defaults to -# request_timeout setting -elasticsearch.pingTimeout: 1500 - -# Time in milliseconds to wait for responses from the back end or elasticsearch. -# This must be > 0 -elasticsearch.requestTimeout: 300000 - -# Time in milliseconds for Elasticsearch to wait for responses from shards. -# Set to 0 to disable. -elasticsearch.shardTimeout: 0 - -# Time in milliseconds to wait for Elasticsearch at Kibana startup before retrying -elasticsearch.startupTimeout: 5000 - -# Set the path to where you would like the process id file to be created. -# pid.file: /var/run/kibana.pid - -# Set this to true to suppress all logging output. -logging.silent: false -# Set this to true to suppress all logging output except for error messages. -logging.quiet: false -# Set this to true to log all events, including system usage information and all requests. -logging.verbose: true - -# monasca-kibana-plugin configuration -monasca-kibana-plugin.auth_uri: %KEYSTONE_AUTH_URI% -monasca-kibana-plugin.enabled: True -monasca-kibana-plugin.cookie.isSecure: False - -optimize.useBundleCache: False diff --git a/devstack/files/monasca-agent/elastic.yaml b/devstack/files/monasca-agent/elastic.yaml deleted file mode 100644 index 269fdae7..00000000 --- a/devstack/files/monasca-agent/elastic.yaml +++ /dev/null @@ -1,3 +0,0 @@ -init_config: -instances: -- url: http://{{IP}}:9200 diff --git a/devstack/files/monasca-agent/http_check.yaml b/devstack/files/monasca-agent/http_check.yaml deleted file mode 100644 index 97424e55..00000000 --- a/devstack/files/monasca-agent/http_check.yaml +++ /dev/null @@ -1,27 +0,0 @@ -init_config: null -instances: -- name: keystone - dimensions: - service: keystone - timeout: 3 - url: http://127.0.0.1/identity -- name: mysql - dimensions: - service: mysql - timeout: 3 - url: http://127.0.0.1:3306 -- name: influxdb - dimensions: - service: influxdb - timeout: 3 - url: http://127.0.0.1:8086/ping -- name: elasticsearch - dimensions: - service: elasticsearch - timeout: 3 - url: http://{{IP}}:9200/_cat/health -- name: kibana - dimensions: - service: kibana - timeout: 3 - url: http://{{IP}}:5601/status diff --git a/devstack/files/monasca-agent/process.yaml b/devstack/files/monasca-agent/process.yaml deleted file mode 100644 index d8878bbd..00000000 --- a/devstack/files/monasca-agent/process.yaml +++ /dev/null @@ -1,143 +0,0 @@ -init_config: - -instances: -- name: influxd - detailed: true - dimensions: - service: influxd - exact_match: false - search_string: - - influxd -- name: monasca-statsd - detailed: true - dimensions: - service: monasca-statsd - exact_match: false - search_string: - - monasca-statsd -- name: monasca-notification - detailed: true - dimensions: - service: monasca-notification - exact_match: false - search_string: - - monasca-notification -- name: persister - detailed: true - dimensions: - service: persister - exact_match: false - search_string: - - persister -- name: storm - detailed: true - dimensions: - service: storm - exact_match: false - search_string: - - storm -- name: monasca-api - detailed: true - dimensions: - service: uwsgi - exact_match: false - search_string: - - uwsgi -- name: monasca-collector - detailed: true - dimensions: - service: monasca-collector - exact_match: false - search_string: - - monasca-collector -- name: memcached - detailed: true - dimensions: - service: memcached - exact_match: false - search_string: - - memcached -- name: monasca-forwarder - detailed: true - dimensions: - service: monasca-forwarder - exact_match: false - search_string: - - monasca-forwarder -- name: zookeeper - detailed: true - dimensions: - service: zookeeper - exact_match: false - search_string: - - zookeeper -- name: kafka - detailed: true - dimensions: - service: kafka - exact_match: false - search_string: - - kafka -- name: mysqld - detailed: true - dimensions: - service: mysqld - exact_match: false - search_string: - - mysqld -- name: logspout - detailed: true - dimensions: - service: logspout - exact_match: false - search_string: - - logspout -- name: log-agent - detailed: true - dimensions: - service: log-agent - exact_match: false - search_string: - - log-agent -- name: log-api - detailed: true - dimensions: - service: log-api - exact_match: false - search_string: - - log-api -- name: kibana - detailed: true - dimensions: - service: kibana - exact_match: false - search_string: - - kibana -- name: elasticsearch - detailed: true - dimensions: - service: elasticsearch - exact_match: false - search_string: - - elasticsearch -- name: log-transformer - detailed: true - dimensions: - service: log-transformer - exact_match: false - search_string: - - log-transformer -- name: log-persister - detailed: true - dimensions: - service: log-persister - exact_match: false - search_string: - - log-persister -- name: log-metrics - detailed: true - dimensions: - service: log-metrics - exact_match: false - search_string: - - log-metrics diff --git a/devstack/files/monasca-log-agent/agent.conf b/devstack/files/monasca-log-agent/agent.conf deleted file mode 100644 index 956fae94..00000000 --- a/devstack/files/monasca-log-agent/agent.conf +++ /dev/null @@ -1,47 +0,0 @@ -# -# Copyright 2016 FUJITSU LIMITED -# -# 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. -# -input { - file { - add_field => { "dimensions" => { "service" => "system" }} - path => "/var/log/syslog" - tags => ["syslog"] - } -} - -filter { - if "syslog" in [tags] { - multiline { - negate => "true" - pattern => "^%{SYSLOGTIMESTAMP}" - what => "previous" - } - } -} - -# TODO(trebskit) should use own user for log-agent -output { - monasca_log_api { - monasca_log_api_url => "%MONASCA_LOG_API_URI_V3%" - keystone_api_url => "%KEYSTONE_AUTH_URI_V3%" - project_name => "mini-mon" - username => "monasca-agent" - password => "password" - user_domain_name => "default" - project_domain_name => "default" - dimensions => [ "hostname:devstack" ] - } -} diff --git a/devstack/files/monasca-log-metrics/log-metrics.conf b/devstack/files/monasca-log-metrics/log-metrics.conf deleted file mode 100644 index b0bb5990..00000000 --- a/devstack/files/monasca-log-metrics/log-metrics.conf +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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. - - -input { - kafka { - zk_connect => "127.0.0.1:2181" - topic_id => "transformed-log" - group_id => "log-metric" - consumer_id => "monasca_log_metrics" - consumer_threads => "4" - } -} - - -filter { - - # drop logs that have not set log level - if "level" not in [log] { - drop { periodic_flush => true } - } else { - ruby { - code => " - log_level = event['log']['level'].downcase - event['log']['level'] = log_level - " - } - } - - # drop logs with log level not in warning,error - if [log][level] not in [warning,error] { - drop { periodic_flush => true } - } - - ruby { - code => " - log_level = event['log']['level'].downcase - log_ts = Time.now.to_f * 1000.0 - - # metric name - metric_name = 'log.%s' % log_level - - # build metric - metric = {} - metric['name'] = metric_name - metric['timestamp'] = log_ts - metric['value'] = 1 - metric['dimensions'] = event['log']['dimensions'] - metric['value_meta'] = {} - - event['metric'] = metric.to_hash - " - } - - mutate { - remove_field => ["log", "@version", "@timestamp", "log_level_original", "tags"] - } - -} - - -output { - kafka { - bootstrap_servers => "%KAFKA_SERVICE_HOST%:%KAFKA_SERVICE_PORT%" - topic_id => "metrics" - client_id => "monasca_log_metrics" - compression_type => "none" - } -} diff --git a/devstack/files/monasca-log-persister/persister.conf b/devstack/files/monasca-log-persister/persister.conf deleted file mode 100644 index c0f581e3..00000000 --- a/devstack/files/monasca-log-persister/persister.conf +++ /dev/null @@ -1,71 +0,0 @@ -# -# Copyright 2016 FUJITSU LIMITED -# -# 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. -# -input { - kafka { - zk_connect => "127.0.0.1:2181" - topic_id => "transformed-log" - group_id => "logstash-persister" - } -} - -filter { - date { - match => ["[log][timestamp]", "UNIX"] - target => "@timestamp" - } - - date { - match => ["creation_time", "UNIX"] - target => "creation_time" - } - - grok { - match => { - "[@timestamp]" => "^(?\d{4}-\d{2}-\d{2})" - } - } - - if "dimensions" in [log] { - ruby { - code => " - fieldHash = event['log']['dimensions'] - fieldHash.each do |key, value| - event[key] = value - end - " - } - } - - mutate { - add_field => { - message => "%{[log][message]}" - log_level => "%{[log][level]}" - tenant => "%{[meta][tenantId]}" - region => "%{[meta][region]}" - } - remove_field => ["@version", "host", "type", "tags" ,"_index_date", "meta", "log"] - } -} - -output { - elasticsearch { - index => "logs-%{tenant}-%{index_date}" - document_type => "log" - hosts => ["%ES_SERVICE_BIND_HOST%"] - flush_size => 500 - } -} diff --git a/devstack/files/monasca-log-transformer/transformer.conf b/devstack/files/monasca-log-transformer/transformer.conf deleted file mode 100644 index 0e2f7c7f..00000000 --- a/devstack/files/monasca-log-transformer/transformer.conf +++ /dev/null @@ -1,87 +0,0 @@ -# -# Copyright 2016 FUJITSU LIMITED -# -# 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. -# -input { - kafka { - zk_connect => "127.0.0.1:2181" - topic_id => "log" - group_id => "transformer-logstash-consumer" - } -} - -filter { - ruby { - code => "event['message_tmp'] = event['log']['message'][0..49]" - } - grok { - match => { - "[message_tmp]" => "(?i)(?AUDIT|CRITICAL|DEBUG|INFO|TRACE|ERR(OR)?|WARN(ING)?)|\"level\":\s?(?\d{2})" - } - } - if ! [log_level] { - grok { - match => { - "[log][message]" => "(?i)(?AUDIT|CRITICAL|DEBUG|INFO|TRACE|ERR(OR)?|WARN(ING)?)|\"level\":\s?(?\d{2})" - } - } - } - ruby { - init => " - LOG_LEVELS_MAP = { - # SYSLOG - 'warn' => :Warning, - 'err' => :Error, - # Bunyan errcodes - '10' => :Trace, - '20' => :Debug, - '30' => :Info, - '40' => :Warning, - '50' => :Error, - '60' => :Fatal - } - " - code => " - if event['log_level'] - # keep original value - log_level = event['log_level'].downcase - if LOG_LEVELS_MAP.has_key?(log_level) - event['log_level_original'] = event['log_level'] - event['log_level'] = LOG_LEVELS_MAP[log_level] - else - event['log_level'] = log_level.capitalize - end - else - event['log_level'] = 'Unknown' - end - " - } - - mutate { - add_field => { - "[log][level]" => "%{log_level}" - } - - # remove temporary fields - remove_field => ["log_level", "message_tmp"] - } -} - -output { - kafka { - bootstrap_servers => "%KAFKA_SERVICE_HOST%:%KAFKA_SERVICE_PORT%" - topic_id => "transformed-log" - } -} diff --git a/devstack/lib/config.sh b/devstack/lib/config.sh deleted file mode 100644 index 83064c24..00000000 --- a/devstack/lib/config.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/bin/bash - -# Copyright 2017 FUJITSU LIMITED -# -# 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. - -# Non configurable settings or settings derived from another settings - -_XTRACE_LOG_API_CONFIG=$(set +o | grep xtrace) -set +o xtrace - -if [[ ${USE_VENV} = True ]]; then - PROJECT_VENV["monasca-log-api"]=${MONASCA_LOG_API_DIR}.venv - MONASCA_LOG_API_BIN_DIR=${PROJECT_VENV["monasca-log-api"]}/bin -else - MONASCA_LOG_API_BIN_DIR=$(get_python_exec_prefix) -fi - -MONASCA_LOG_API_WSGI=$MONASCA_LOG_API_BIN_DIR/monasca-log-api-wsgi -MONASCA_LOG_API_DEPLOY=`determine_log_api_deploy_mode` -if is_service_enabled tls-proxy; then - MONASCA_LOG_API_SERVICE_PROTOCOL="https" -fi -if [ "$MONASCA_LOG_API_USE_MOD_WSGI" == "True" ]; then - MONASCA_LOG_API_BASE_URI=${MONASCA_LOG_API_SERVICE_PROTOCOL}://${MONASCA_LOG_API_SERVICE_HOST}/logs -else - MONASCA_LOG_API_BASE_URI=${MONASCA_LOG_API_SERVICE_PROTOCOL}://${MONASCA_LOG_API_SERVICE_HOST}:${MONASCA_LOG_API_SERVICE_PORT} -fi -MONASCA_LOG_API_URI_V2=${MONASCA_LOG_API_BASE_URI}/v2.0 -MONASCA_LOG_API_URI_V3=${MONASCA_LOG_API_BASE_URI}/v3.0 - -MONASCA_LOG_API_CONF_DIR=${MONASCA_LOG_API_CONF_DIR:-/etc/monasca} -MONASCA_LOG_API_LOG_DIR=${MONASCA_LOG_API_LOG_DIR:-/var/log/monasca} -MONASCA_LOG_API_CACHE_DIR=${MONASCA_LOG_API_CACHE_DIR:-/var/cache/monasca-log-api} -MONASCA_LOG_API_WSGI_DIR=${MONASCA_LOG_API_WSGI_DIR:-/var/www/monasca-log-api} - -MONASCA_LOG_API_CONF=${MONASCA_LOG_API_CONF:-$MONASCA_LOG_API_CONF_DIR/monasca-log-api.conf} -MONASCA_LOG_API_PASTE=${MONASCA_LOG_API_PASTE:-$MONASCA_LOG_API_CONF_DIR/log-api-paste.ini} -MONASCA_LOG_API_LOGGING_CONF=${MONASCA_LOG_API_LOGGING_CONF:-$MONASCA_LOG_API_CONF_DIR/log-api-logging.conf} -MONASCA_LOG_API_UWSGI_CONF=${MONASCA_LOG_API_UWSGI_CONF:-$MONASCA_LOG_API_CONF_DIR/log-api-uwsgi.ini} - -MONASCA_LOG_API_USE_MOD_WSGI=${MONASCA_LOG_API_USE_MOD_WSGI:-$ENABLE_HTTPD_MOD_WSGI_SERVICES} - -# configuration bits of various services -LOG_PERSISTER_DIR=$DEST/monasca-log-persister -LOG_TRANSFORMER_DIR=$DEST/monasca-log-transformer -LOG_METRICS_DIR=$DEST/monasca-log-metrics -LOG_AGENT_DIR=$DEST/monasca-log-agent - -ELASTICSEARCH_DIR=$DEST/elasticsearch -ELASTICSEARCH_CFG_DIR=$ELASTICSEARCH_DIR/config -ELASTICSEARCH_LOG_DIR=$LOGDIR/elasticsearch -ELASTICSEARCH_DATA_DIR=$DATA_DIR/elasticsearch - -KIBANA_DIR=$DEST/kibana -KIBANA_CFG_DIR=$KIBANA_DIR/config - -LOGSTASH_DIR=$DEST/logstash - -PLUGIN_FILES=$MONASCA_LOG_API_DIR/devstack/files -# configuration bits of various services - -# Files inside this directory will be visible in gates log -GATE_CONFIGURATION_DIR=/etc/monasca-log-api - -# clone monasca-{common,statsd} directly from repo -GITREPO["monasca-common"]=${MONASCA_COMMON_REPO} -GITBRANCH["monasca-common"]=${MONASCA_COMMON_BRANCH} -GITDIR["monasca-common"]=${MONASCA_COMMON_DIR} - -GITREPO["monasca-statsd"]=${MONASCA_STATSD_REPO} -GITBRANCH["monasca-statsd"]=${MONASCA_STATSD_BRANCH} -GITDIR["monasca-statsd"]=${MONASCA_STATSD_DIR} - -LIBS_FROM_GIT="${LIBS_FROM_GIT:-""},monasca-common,monasca-statsd" -# clone monasca-{common,statsd} directly from repo - -# public facing bits -MONASCA_LOG_API_SERVICE_HOST=${MONASCA_LOG_API_SERVICE_HOST:-${SERVICE_HOST}} -MONASCA_LOG_API_SERVICE_PORT=${MONASCA_LOG_API_SERVICE_PORT:-5607} -MONASCA_LOG_API_SERVICE_PORT_INT=${MONASCA_LOG_API_SERVICE_PORT:-15607} -MONASCA_LOG_API_SERVICE_PROTOCOL=${MONASCA_LOG_API_SERVICE_PROTOCOL:-${SERVICE_PROTOCOL}} - -ES_SERVICE_BIND_HOST=${ES_SERVICE_BIND_HOST:-${SERVICE_HOST}} -ES_SERVICE_BIND_PORT=${ES_SERVICE_BIND_PORT:-9200} -ES_SERVICE_PUBLISH_HOST=${ES_SERVICE_PUBLISH_HOST:-${SERVICE_HOST}} -ES_SERVICE_PUBLISH_PORT=${ES_SERVICE_PUBLISH_PORT:-9300} - -KIBANA_SERVICE_HOST=${KIBANA_SERVICE_HOST:-${SERVICE_HOST}} -KIBANA_SERVICE_PORT=${KIBANA_SERVICE_PORT:-5601} -KIBANA_SERVER_BASE_PATH=${KIBANA_SERVER_BASE_PATH:-"/dashboard/monitoring/logs_proxy"} - -KAFKA_SERVICE_HOST=${KAFKA_SERVICE_HOST:-${SERVICE_HOST}} -KAFKA_SERVICE_PORT=${KAFKA_SERVICE_PORT:-9092} -# public facing bits - -${_XTRACE_LOG_API_CONFIG} diff --git a/devstack/lib/util.sh b/devstack/lib/util.sh deleted file mode 100644 index b2612964..00000000 --- a/devstack/lib/util.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -# Copyright 2017 FUJITSU LIMITED -# -# 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. - -# Set of utility-like methods that are not bound to any particular -# service - -_XTRACE_LOG_API_UTILS=$(set +o | grep xtrace) -set +o xtrace - -run_process_sleep() { - local name=$1 - local cmd=$2 - local sleepTime=${3:-1} - run_process "$name" "$cmd" - sleep ${sleepTime} -} - -is_logstash_required() { - is_service_enabled monasca-log-persister \ - || is_service_enabled monasca-log-transformer \ - || is_service_enabled monasca-log-metrics \ - || is_service_enabled monasca-log-agent \ - && return 0 -} - -# MONASCA_LOG_API_DEPLOY defines how monasca-log-api is deployed, allowed values: -# - mod_wsgi : Run monasca-log-api under Apache HTTPd mod_wsgi -# - uwsgi : Run monasca-log-api under uwsgi -# - gunicorn: Run monasca-log-api under gunicorn -determine_log_api_deploy_mode() { - MONASCA_LOG_API_USE_MOD_WSGI=$(trueorfalse False MONASCA_LOG_API_USE_MOD_WSGI) - if [ "$MONASCA_LOG_API_USE_MOD_WSGI" == "True" ]; then - if [[ "$WSGI_MODE" == "uwsgi" ]]; then - echo "uwsgi" - else - echo "mod_wsgi" - fi - else - echo "gunicorn" - fi -} - -${_XTRACE_LOG_API_UTILS} diff --git a/devstack/plugin.sh b/devstack/plugin.sh deleted file mode 100644 index ff1fcd1d..00000000 --- a/devstack/plugin.sh +++ /dev/null @@ -1,862 +0,0 @@ -#!/bin/bash - -# -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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. -# - -# Save trace setting -_XTRACE_LOG_API=$(set +o | grep xtrace) -set -o xtrace - -_ERREXIT_LOG_API=$(set +o | grep errexit) -set -o errexit - -# source lib/* -source ${MONASCA_LOG_API_DIR}/devstack/lib/util.sh -source ${MONASCA_LOG_API_DIR}/devstack/lib/config.sh -# source lib/* - -# TOP_LEVEL functions called from devstack coordinator -############################################################################### -function pre_install { - install_elk - install_nodejs - install_gate_config_holder -} - -function install_monasca_log { - build_kibana_plugin - if is_service_enabled monasca-log-api; then - # install_monasca-log-api is not called directly - # stack_install_service calls it - if python3_enabled; then - enable_python3_package monasca-log-api - fi - stack_install_service monasca-log-api - fi - install_log_agent -} - -function install_elk { - install_logstash - install_elasticsearch - install_kibana -} - -function install_gate_config_holder { - sudo install -d -o $STACK_USER $GATE_CONFIGURATION_DIR -} - -function install_monasca_common { - if use_library_from_git "monasca-common"; then - git_clone_by_name "monasca-common" - setup_dev_lib "monasca-common" - fi -} - -function install_monasca_statsd { - if use_library_from_git "monasca-statsd"; then - git_clone_by_name "monasca-statsd" - setup_dev_lib "monasca-statsd" - fi -} - -function configure_monasca_log { - configure_kafka - configure_elasticsearch - configure_kibana - install_kibana_plugin - - configure_monasca_log_api - configure_monasca_log_transformer - configure_monasca_log_metrics - configure_monasca_log_persister - configure_monasca_log_agent -} - -function init_monasca_log { - enable_log_management - create_log_management_accounts -} - -function init_monasca_grafana_dashboards { - if is_service_enabled horizon; then - echo_summary "Init Grafana dashboards" - - sudo python3 "${PLUGIN_FILES}"/grafana/grafana.py "${PLUGIN_FILES}"/grafana/dashboards.d - fi -} - -function init_agent { - echo_summary "Init Monasca agent" - - sudo cp -f "${PLUGIN_FILES}"/monasca-agent/http_check.yaml /etc/monasca/agent/conf.d/http_check.yaml - sudo cp -f "${PLUGIN_FILES}"/monasca-agent/process.yaml /etc/monasca/agent/conf.d/process.yaml - sudo cp -f "${PLUGIN_FILES}"/monasca-agent/elastic.yaml /etc/monasca/agent/conf.d/elastic.yaml - - sudo sed -i "s/{{IP}}/$(ip -o -4 addr list eth1 | awk '{print $4}' | cut -d/ -f1 | head -1)/" /etc/monasca/agent/conf.d/*.yaml - sudo sed -i "s/127\.0\.0\.1/$(hostname)/" /etc/monasca/agent/conf.d/*.yaml - sudo systemctl restart monasca-collector -} - -function stop_monasca_log { - stop_process "monasca-log-agent" || true - stop_monasca_log_api - stop_process "monasca-log-metrics" || true - stop_process "monasca-log-persister" || true - stop_process "monasca-log-transformer" || true - stop_process "kibana" || true - stop_process "elasticsearch" || true -} - -function start_monasca_log { - start_elasticsearch - start_kibana - start_monasca_log_transformer - start_monasca_log_metrics - start_monasca_log_persister - start_monasca_log_api - start_monasca_log_agent -} - -function clean_monasca_log { - clean_monasca_log_agent - clean_monasca_log_api - clean_monasca_log_persister - clean_monasca_log_transformer - clean_kibana - clean_elasticsearch - clean_logstash - clean_nodejs - clean_gate_config_holder -} -############################################################################### - -function install_monasca-log-api { - echo_summary "Installing monasca-log-api" - - git_clone $MONASCA_LOG_API_REPO $MONASCA_LOG_API_DIR $MONASCA_LOG_API_BRANCH - setup_develop $MONASCA_LOG_API_DIR - - install_keystonemiddleware - install_monasca_common - install_monasca_statsd - - if [ "$MONASCA_LOG_API_DEPLOY" == "mod_wsgi" ]; then - install_apache_wsgi - elif [ "$MONASCA_LOG_API_DEPLOY" == "uwsgi" ]; then - pip_install uwsgi - else - pip_install gunicorn - fi - - if [ "$MONASCA_LOG_API_DEPLOY" != "gunicorn" ]; then - if is_ssl_enabled_service "monasca-log-api"; then - enable_mod_ssl - fi - fi -} - -function configure_monasca_log_api { - if is_service_enabled monasca-log-api; then - echo_summary "Configuring monasca-log-api" - - configure_monasca_log_api_core - if [ "$MONASCA_LOG_API_DEPLOY" == "mod_wsgi" ]; then - configure_monasca_log_api_mod_wsgi - elif [ "$MONASCA_LOG_API_DEPLOY" == "uwsgi" ]; then - configure_monasca_log_api_uwsgi - fi - - # link configuration for the gate - ln -sf $MONASCA_LOG_API_CONF $GATE_CONFIGURATION_DIR - ln -sf $MONASCA_LOG_API_PASTE $GATE_CONFIGURATION_DIR - ln -sf $MONASCA_LOG_API_LOGGING_CONF $GATE_CONFIGURATION_DIR - - fi -} - -function configure_monasca_log_api_core { - # Put config files in ``$MONASCA_LOG_API_CONF_DIR`` for everyone to find - sudo install -d -o $STACK_USER $MONASCA_LOG_API_CONF_DIR - sudo install -m 700 -d -o $STACK_USER $MONASCA_LOG_API_CACHE_DIR - sudo install -d -o $STACK_USER $MONASCA_LOG_API_LOG_DIR - - # ensure fresh installation of configuration files - rm -rf $MONASCA_LOG_API_CONF $MONASCA_LOG_API_PASTE $MONASCA_LOG_API_LOGGING_CONF - - $MONASCA_LOG_API_BIN_DIR/oslo-config-generator \ - --config-file $MONASCA_LOG_API_DIR/config-generator/monasca-log-api.conf \ - --output-file /tmp/monasca-log-api.conf - - install -m 600 /tmp/monasca-log-api.conf $MONASCA_LOG_API_CONF && rm -rf /tmp/monasca-log-api.conf - install -m 600 $MONASCA_LOG_API_DIR/etc/monasca/log-api-paste.ini $MONASCA_LOG_API_PASTE - install -m 600 $MONASCA_LOG_API_DIR/etc/monasca/log-api-logging.conf $MONASCA_LOG_API_LOGGING_CONF - - # configure monasca-log-api.conf - iniset "$MONASCA_LOG_API_CONF" DEFAULT log_config_append $MONASCA_LOG_API_LOGGING_CONF - iniset "$MONASCA_LOG_API_CONF" service region $REGION_NAME - - iniset "$MONASCA_LOG_API_CONF" log_publisher kafka_url $KAFKA_SERVICE_HOST:$KAFKA_SERVICE_PORT - iniset "$MONASCA_LOG_API_CONF" log_publisher topics log - - iniset "$MONASCA_LOG_API_CONF" kafka_healthcheck kafka_url $KAFKA_SERVICE_HOST:$KAFKA_SERVICE_PORT - iniset "$MONASCA_LOG_API_CONF" kafka_healthcheck kafka_topics log - - iniset "$MONASCA_LOG_API_CONF" roles_middleware path "/v2.0/log,/v3.0/logs" - iniset "$MONASCA_LOG_API_CONF" roles_middleware default_roles monasca-user - iniset "$MONASCA_LOG_API_CONF" roles_middleware agent_roles monasca-agent - iniset "$MONASCA_LOG_API_CONF" roles_middleware delegate_roles admin - - # configure keystone middleware - configure_auth_token_middleware "$MONASCA_LOG_API_CONF" "admin" $MONASCA_LOG_API_CACHE_DIR - iniset "$MONASCA_LOG_API_CONF" keystone_authtoken region_name $REGION_NAME - iniset "$MONASCA_LOG_API_CONF" keystone_authtoken project_name "admin" - iniset "$MONASCA_LOG_API_CONF" keystone_authtoken password $ADMIN_PASSWORD - - # insecure - if is_service_enabled tls-proxy; then - iniset "$MONASCA_LOG_API_CONF" keystone_authtoken insecure False - fi - - # configure log-api-paste.ini - iniset "$MONASCA_LOG_API_PASTE" server:main bind $MONASCA_LOG_API_SERVICE_HOST:$MONASCA_LOG_API_SERVICE_PORT - iniset "$MONASCA_LOG_API_PASTE" server:main chdir $MONASCA_LOG_API_DIR - iniset "$MONASCA_LOG_API_PASTE" server:main workers $API_WORKERS -} - -function configure_monasca_log_api_uwsgi { - rm -rf $MONASCA_LOG_API_UWSGI_CONF - install -m 600 $MONASCA_LOG_API_DIR/etc/monasca/log-api-uwsgi.ini $MONASCA_LOG_API_UWSGI_CONF - - write_uwsgi_config "$MONASCA_LOG_API_UWSGI_CONF" "$MONASCA_LOG_API_WSGI" "/logs" -} - -function configure_monasca_log_api_mod_wsgi { - sudo install -d $MONASCA_LOG_API_WSGI_DIR - - local monasca_log_api_apache_conf - monasca_log_api_apache_conf=$(apache_site_config_for monasca-log-api) - - local monasca_log_api_ssl="" - local monasca_log_api_certfile="" - local monasca_log_api_keyfile="" - local monasca_log_api_api_port=$MONASCA_LOG_API_SERVICE_PORT - local venv_path="" - - if is_ssl_enabled_service monasca_log_api; then - monasca_log_api_ssl="SSLEngine On" - monasca_log_api_certfile="SSLCertificateFile $MONASCA_LOG_API_SSL_CERT" - monasca_log_api_keyfile="SSLCertificateKeyFile $MONASCA_LOG_API_SSL_KEY" - fi - if is_service_enabled tls-proxy; then - monasca_log_api_api_port=$MONASCA_LOG_API_SERVICE_PORT_INT - fi - if [[ ${USE_VENV} = True ]]; then - venv_path="python-path=${PROJECT_VENV["monasca_log_api"]}/lib/$(python_version)/site-packages" - fi - - # copy proxy vhost and wsgi helper files - sudo cp $PLUGIN_FILES/apache-log-api.template $monasca_log_api_apache_conf - sudo sed -e " - s|%PUBLICPORT%|$monasca_log_api_api_port|g; - s|%APACHE_NAME%|$APACHE_NAME|g; - s|%PUBLICWSGI%|$MONASCA_LOG_API_BIN_DIR/monasca-log-api-wsgi|g; - s|%SSLENGINE%|$monasca_log_api_ssl|g; - s|%SSLCERTFILE%|$monasca_log_api_certfile|g; - s|%SSLKEYFILE%|$monasca_log_api_keyfile|g; - s|%USER%|$STACK_USER|g; - s|%VIRTUALENV%|$venv_path|g - s|%APIWORKERS%|$API_WORKERS|g - " -i $monasca_log_api_apache_conf -} - -function create_log_api_cache_dir { - sudo install -m 700 -d -o $STACK_USER $MONASCA_LOG_API_CACHE_DIR -} - -function clean_monasca_log_api { - if is_service_enabled monasca-log-api; then - echo_summary "Cleaning monasca-log-api" - - sudo rm -f $MONASCA_LOG_API_CONF || true - sudo rm -f $MONASCA_LOG_API_PASTE || true - sudo rm -f $MONASCA_LOG_API_LOGGING_CONF || true - sudo rm -rf $MONASCA_LOG_API_CACHE_DIR || true - sudo rm -rf $MONASCA_LOG_API_CONF_DIR || true - - sudo rm -rf $MONASCA_LOG_API_DIR || true - - if [ "$MONASCA_LOG_API_USE_MOD_WSGI" == "True" ]; then - clean_monasca_log_api_wsgi - fi - fi -} - -function clean_monasca_log_api_wsgi { - sudo rm -f $MONASCA_LOG_API_WSGI_DIR/* - sudo rm -f $(apache_site_config_for monasca-log-api) -} - -function start_monasca_log_api { - if is_service_enabled monasca-log-api; then - echo_summary "Starting monasca-log-api" - - local service_port=$MONASCA_LOG_API_SERVICE_PORT - local service_protocol=$MONASCA_LOG_API_SERVICE_PROTOCOL - if is_service_enabled tls-proxy; then - service_port=$MONASCA_LOG_API_SERVICE_PORT_INT - service_protocol="http" - fi - local service_uri - - if [ "$MONASCA_LOG_API_DEPLOY" == "mod_wsgi" ]; then - local enabled_site_file - enabled_site_file=$(apache_site_config_for monasca-log-api) - service_uri=$service_protocol://$MONASCA_LOG_API_SERVICE_HOST/logs/v3.0 - if [ -f ${enabled_site_file} ]; then - enable_apache_site monasca-log-api - restart_apache_server - tail_log monasca-log-api /var/log/$APACHE_NAME/monasca-log-api.log - fi - elif [ "$MONASCA_LOG_API_DEPLOY" == "uwsgi" ]; then - service_uri=$service_protocol://$MONASCA_LOG_API_SERVICE_HOST/logs/v3.0 - run_process "monasca-log-api" "$MONASCA_LOG_API_BIN_DIR/uwsgi --ini $MONASCA_LOG_API_UWSGI_CONF" "" - else - service_uri=$service_protocol://$MONASCA_LOG_API_SERVICE_HOST:$service_port - run_process "monasca-log-api" "$MONASCA_LOG_API_BIN_DIR/gunicorn --paste $MONASCA_LOG_API_PASTE" "" - fi - - echo "Waiting for monasca-log-api to start..." - if ! wait_for_service $SERVICE_TIMEOUT $service_uri; then - die $LINENO "monasca-log-api did not start" - fi - - if is_service_enabled tls-proxy; then - start_tls_proxy monasca-log-api '*' $MONASCA_LOG_API_SERVICE_PORT $MONASCA_LOG_API_SERVICE_HOST $MONASCA_LOG_API_SERVICE_PORT_INT - fi - - restart_service memcached - fi -} - -function stop_monasca_log_api { - if is_service_enabled monasca-log-api; then - if [ "$MONASCA_LOG_API_DEPLOY" == "mod_wsgi" ]; then - disable_apache_site monasca-log-api - restart_apache_server - else - stop_process "monasca-log-api" - if [ "$MONASCA_LOG_API_DEPLOY" == "uwsgi" ]; then - remove_uwsgi_config "$MONASCA_LOG_API_UWSGI_CONF" "$MONASCA_LOG_API_WSGI" - fi - fi - fi -} - -function install_logstash { - if is_logstash_required; then - echo_summary "Installing Logstash ${LOGSTASH_VERSION}" - - local logstash_tarball=logstash-${LOGSTASH_VERSION}.tar.gz - local logstash_url=http://download.elastic.co/logstash/logstash/${logstash_tarball} - - local logstash_dest - logstash_dest=`get_extra_file ${logstash_url}` - - tar xzf ${logstash_dest} -C $DEST - - sudo chown -R $STACK_USER $DEST/logstash-${LOGSTASH_VERSION} - ln -sf $DEST/logstash-${LOGSTASH_VERSION} $LOGSTASH_DIR - fi -} - -function clean_logstash { - if is_logstash_required; then - echo_summary "Cleaning Logstash ${LOGSTASH_VERSION}" - - sudo rm -rf $LOGSTASH_DIR || true - sudo rm -rf $FILES/logstash-${LOGSTASH_VERSION}.tar.gz || true - sudo rm -rf $DEST/logstash-${LOGSTASH_VERSION} || true - fi -} - -function install_elasticsearch { - if is_service_enabled elasticsearch; then - echo_summary "Installing ElasticSearch ${ELASTICSEARCH_VERSION}" - - local es_tarball=elasticsearch-${ELASTICSEARCH_VERSION}.tar.gz - local es_url=https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/${ELASTICSEARCH_VERSION}/${es_tarball} - - local es_dest - es_dest=`get_extra_file ${es_url}` - - tar xzf ${es_dest} -C $DEST - - sudo chown -R $STACK_USER $DEST/elasticsearch-${ELASTICSEARCH_VERSION} - ln -sf $DEST/elasticsearch-${ELASTICSEARCH_VERSION} $ELASTICSEARCH_DIR - fi -} - -function configure_elasticsearch { - if is_service_enabled elasticsearch; then - echo_summary "Configuring ElasticSearch ${ELASTICSEARCH_VERSION}" - - local templateDir=$ELASTICSEARCH_CFG_DIR/templates - - for dir in $ELASTICSEARCH_LOG_DIR $templateDir $ELASTICSEARCH_DATA_DIR; do - sudo install -m 755 -d -o $STACK_USER $dir - done - - sudo cp -f "${PLUGIN_FILES}"/elasticsearch/elasticsearch.yml $ELASTICSEARCH_CFG_DIR/elasticsearch.yml - sudo chown -R $STACK_USER $ELASTICSEARCH_CFG_DIR/elasticsearch.yml - sudo chmod 0644 $ELASTICSEARCH_CFG_DIR/elasticsearch.yml - - sudo sed -e " - s|%ES_SERVICE_BIND_HOST%|$ES_SERVICE_BIND_HOST|g; - s|%ES_SERVICE_BIND_PORT%|$ES_SERVICE_BIND_PORT|g; - s|%ES_SERVICE_PUBLISH_HOST%|$ES_SERVICE_PUBLISH_HOST|g; - s|%ES_SERVICE_PUBLISH_PORT%|$ES_SERVICE_PUBLISH_PORT|g; - s|%ES_DATA_DIR%|$ELASTICSEARCH_DATA_DIR|g; - s|%ES_LOG_DIR%|$ELASTICSEARCH_LOG_DIR|g; - " -i $ELASTICSEARCH_CFG_DIR/elasticsearch.yml - - ln -sf $ELASTICSEARCH_CFG_DIR/elasticsearch.yml $GATE_CONFIGURATION_DIR/elasticsearch.yml - fi -} - -function clean_elasticsearch { - if is_service_enabled elasticsearch; then - echo_summary "Cleaning Elasticsearch ${ELASTICSEARCH_VERSION}" - - sudo rm -rf ELASTICSEARCH_DIR || true - sudo rm -rf ELASTICSEARCH_CFG_DIR || true - sudo rm -rf ELASTICSEARCH_LOG_DIR || true - sudo rm -rf ELASTICSEARCH_DATA_DIR || true - sudo rm -rf $FILES/elasticsearch-${ELASTICSEARCH_VERSION}.tar.gz || true - sudo rm -rf $DEST/elasticsearch-${ELASTICSEARCH_VERSION} || true - fi -} - -function start_elasticsearch { - if is_service_enabled elasticsearch; then - echo_summary "Starting ElasticSearch ${ELASTICSEARCH_VERSION}" - # 5 extra seconds to ensure that ES started properly - local esSleepTime=${ELASTICSEARCH_SLEEP_TIME:-5} - run_process_sleep "elasticsearch" "$ELASTICSEARCH_DIR/bin/elasticsearch" $esSleepTime - fi -} - -function _get_kibana_version_name { - echo "kibana-${KIBANA_VERSION}-linux-x86_64" -} - -function install_kibana { - if is_service_enabled kibana; then - echo_summary "Installing Kibana ${KIBANA_VERSION}" - - local kibana_version_name - kibana_version_name=`_get_kibana_version_name` - local kibana_tarball=${kibana_version_name}.tar.gz - local kibana_tarball_url=http://download.elastic.co/kibana/kibana/${kibana_tarball} - - local kibana_tarball_dest - kibana_tarball_dest=`get_extra_file ${kibana_tarball_url}` - - tar xzf ${kibana_tarball_dest} -C $DEST - - sudo chown -R $STACK_USER $DEST/${kibana_version_name} - ln -sf $DEST/${kibana_version_name} $KIBANA_DIR - fi -} - -function configure_kibana { - if is_service_enabled kibana; then - echo_summary "Configuring Kibana ${KIBANA_VERSION}" - - sudo install -m 755 -d -o $STACK_USER $KIBANA_CFG_DIR - - sudo cp -f "${PLUGIN_FILES}"/kibana/kibana.yml $KIBANA_CFG_DIR/kibana.yml - sudo chown -R $STACK_USER $KIBANA_CFG_DIR/kibana.yml - sudo chmod 0644 $KIBANA_CFG_DIR/kibana.yml - - sudo sed -e " - s|%KIBANA_SERVICE_HOST%|$KIBANA_SERVICE_HOST|g; - s|%KIBANA_SERVICE_PORT%|$KIBANA_SERVICE_PORT|g; - s|%KIBANA_SERVER_BASE_PATH%|$KIBANA_SERVER_BASE_PATH|g; - s|%ES_SERVICE_BIND_HOST%|$ES_SERVICE_BIND_HOST|g; - s|%ES_SERVICE_BIND_PORT%|$ES_SERVICE_BIND_PORT|g; - s|%KEYSTONE_AUTH_URI%|$KEYSTONE_AUTH_URI|g; - " -i $KIBANA_CFG_DIR/kibana.yml - - ln -sf $KIBANA_CFG_DIR/kibana.yml $GATE_CONFIGURATION_DIR/kibana.yml - fi -} - -function install_kibana_plugin { - if is_service_enabled kibana; then - echo_summary "Install Kibana plugin" - - # note(trebskit) that needs to happen after kibana received - # its configuration otherwise the plugin fails to be installed - - local pkg=file://$DEST/monasca-kibana-plugin.tar.gz - - $KIBANA_DIR/bin/kibana plugin -r monasca-kibana-plugin - $KIBANA_DIR/bin/kibana plugin -i monasca-kibana-plugin -u $pkg - fi -} - -function clean_kibana { - if is_service_enabled kibana; then - echo_summary "Cleaning Kibana ${KIBANA_VERSION}" - - local kibana_tarball - kibana_tarball=`_get_kibana_version_name`.tar.gz - sudo rm -rf $KIBANA_DIR || true - sudo rm -rf $FILES/${kibana_tarball} || true - sudo rm -rf $KIBANA_CFG_DIR || true - fi -} - -function start_kibana { - if is_service_enabled kibana; then - echo_summary "Starting Kibana ${KIBANA_VERSION}" - local kibanaSleepTime=${KIBANA_SLEEP_TIME:-90} # kibana takes some time to load up - local kibanaCFG="$KIBANA_CFG_DIR/kibana.yml" - run_process_sleep "kibana" "$KIBANA_DIR/bin/kibana --config $kibanaCFG" $kibanaSleepTime - fi -} - -function configure_monasca_log_persister { - if is_service_enabled monasca-log-persister; then - echo_summary "Configuring monasca-log-persister" - - sudo install -m 755 -d -o $STACK_USER $LOG_PERSISTER_DIR - - sudo cp -f "${PLUGIN_FILES}"/monasca-log-persister/persister.conf $LOG_PERSISTER_DIR/persister.conf - sudo chown $STACK_USER $LOG_PERSISTER_DIR/persister.conf - sudo chmod 0640 $LOG_PERSISTER_DIR/persister.conf - - sudo sed -e " - s|%ES_SERVICE_BIND_HOST%|$ES_SERVICE_BIND_HOST|g; - " -i $LOG_PERSISTER_DIR/persister.conf - - ln -sf $LOG_PERSISTER_DIR/persister.conf $GATE_CONFIGURATION_DIR/log-persister.conf - fi -} - -function clean_monasca_log_persister { - if is_service_enabled monasca-log-persister; then - echo_summary "Cleaning monasca-log-persister" - sudo rm -rf $LOG_PERSISTER_DIR || true - fi -} - -function start_monasca_log_persister { - if is_service_enabled monasca-log-persister; then - echo_summary "Starting monasca-log-persister" - local logstash="$LOGSTASH_DIR/bin/logstash" - run_process "monasca-log-persister" "$logstash -f $LOG_PERSISTER_DIR/persister.conf" - fi -} - -function configure_monasca_log_transformer { - if is_service_enabled monasca-log-transformer; then - echo_summary "Configuring monasca-log-transformer" - - sudo install -m 755 -d -o $STACK_USER $LOG_TRANSFORMER_DIR - - sudo cp -f "${PLUGIN_FILES}"/monasca-log-transformer/transformer.conf $LOG_TRANSFORMER_DIR/transformer.conf - sudo chown $STACK_USER $LOG_TRANSFORMER_DIR/transformer.conf - sudo chmod 0640 $LOG_TRANSFORMER_DIR/transformer.conf - - sudo sed -e " - s|%KAFKA_SERVICE_HOST%|$KAFKA_SERVICE_HOST|g; - s|%KAFKA_SERVICE_PORT%|$KAFKA_SERVICE_PORT|g; - " -i $LOG_TRANSFORMER_DIR/transformer.conf - - ln -sf $LOG_TRANSFORMER_DIR/transformer.conf $GATE_CONFIGURATION_DIR/log-transformer.conf - fi -} - -function clean_monasca_log_transformer { - if is_service_enabled monasca-log-transformer; then - echo_summary "Cleaning monasca-log-transformer" - sudo rm -rf $LOG_TRANSFORMER_DIR || true - fi -} - -function start_monasca_log_transformer { - if is_service_enabled monasca-log-transformer; then - echo_summary "Starting monasca-log-transformer" - local logstash="$LOGSTASH_DIR/bin/logstash" - run_process "monasca-log-transformer" "$logstash -f $LOG_TRANSFORMER_DIR/transformer.conf" - fi -} - -function configure_monasca_log_metrics { - if is_service_enabled monasca-log-metrics; then - echo_summary "Configuring monasca-log-metrics" - - sudo install -m 755 -d -o $STACK_USER $LOG_METRICS_DIR - - sudo cp -f "${PLUGIN_FILES}"/monasca-log-metrics/log-metrics.conf $LOG_METRICS_DIR/log-metrics.conf - sudo chown $STACK_USER $LOG_METRICS_DIR/log-metrics.conf - sudo chmod 0640 $LOG_METRICS_DIR/log-metrics.conf - - sudo sed -e " - s|%KAFKA_SERVICE_HOST%|$KAFKA_SERVICE_HOST|g; - s|%KAFKA_SERVICE_PORT%|$KAFKA_SERVICE_PORT|g; - " -i $LOG_METRICS_DIR/log-metrics.conf - - ln -sf $LOG_METRICS_DIR/log-metrics.conf $GATE_CONFIGURATION_DIR/log-metrics.conf - fi -} - -function clean_monasca_log_metrics { - if is_service_enabled monasca-log-metrics; then - echo_summary "Cleaning monasca-log-metrics" - sudo rm -rf $LOG_METRICS_DIR || true - fi -} - -function start_monasca_log_metrics { - if is_service_enabled monasca-log-metrics; then - echo_summary "Starting monasca-log-metrics" - local logstash="$LOGSTASH_DIR/bin/logstash" - run_process "monasca-log-metrics" "$logstash -f $LOG_METRICS_DIR/log-metrics.conf" - fi -} - -function install_log_agent { - if is_service_enabled monasca-log-agent; then - echo_summary "Installing monasca-log-agent [monasca-output-plugin]" - - $LOGSTASH_DIR/bin/plugin install --version \ - "${LOGSTASH_OUTPUT_MONASCA_VERSION}" logstash-output-monasca_log_api - fi -} - -function configure_monasca_log_agent { - if is_service_enabled monasca-log-agent; then - echo_summary "Configuring monasca-log-agent" - - sudo install -m 755 -d -o $STACK_USER $LOG_AGENT_DIR - - sudo cp -f "${PLUGIN_FILES}"/monasca-log-agent/agent.conf $LOG_AGENT_DIR/agent.conf - sudo chown $STACK_USER $LOG_AGENT_DIR/agent.conf - sudo chmod 0640 $LOG_AGENT_DIR/agent.conf - - sudo sed -e " - s|%MONASCA_LOG_API_URI_V3%|$MONASCA_LOG_API_URI_V3|g; - s|%KEYSTONE_AUTH_URI_V3%|$KEYSTONE_AUTH_URI_V3|g; - " -i $LOG_AGENT_DIR/agent.conf - - ln -sf $LOG_AGENT_DIR/agent.conf $GATE_CONFIGURATION_DIR/log-agent.conf - - fi -} - -function clean_monasca_log_agent { - if is_service_enabled monasca-log-agent; then - echo_summary "Cleaning monasca-log-agent" - sudo rm -rf $LOG_AGENT_DIR || true - fi -} - -function start_monasca_log_agent { - if is_service_enabled monasca-log-agent; then - echo_summary "Starting monasca-log-agent" - local logstash="$LOGSTASH_DIR/bin/logstash" - run_process "monasca-log-agent" "$logstash -f $LOG_AGENT_DIR/agent.conf" "root" "root" - fi -} - -function install_nodejs { - if is_service_enabled kibana; then - # refresh installation - curl -sL https://deb.nodesource.com/setup_10.x | sudo bash - - apt_get install nodejs - ( - npm config set registry "http://registry.npmjs.org/"; \ - npm config set proxy "${HTTP_PROXY}"; \ - npm set strict-ssl false; - ) - fi -} - -function clean_nodejs { - if is_service_enabled kibana; then - echo_summary "Cleaning Node.js" - apt_get purge nodejs - fi -} - -function clean_gate_config_holder { - sudo rm -rf $GATE_CONFIGURATION_DIR || true -} - -function build_kibana_plugin { - if is_service_enabled kibana; then - echo "Building Kibana plugin" - - git_clone $MONASCA_KIBANA_PLUGIN_REPO \ - $MONASCA_KIBANA_PLUGIN_DIR \ - $MONASCA_KIBANA_PLUGIN_BRANCH - - pushd $MONASCA_KIBANA_PLUGIN_DIR - git_update_branch $MONASCA_KIBANA_PLUGIN_BRANCH - - local monasca_kibana_plugin_version - monasca_kibana_plugin_version="$(python -c 'import json; \ - obj = json.load(open("package.json")); print obj["version"]')" - - npm install - npm run package - - local pkg=$MONASCA_KIBANA_PLUGIN_DIR/target/monasca-kibana-plugin-${monasca_kibana_plugin_version}.tar.gz - local easyPkg=$DEST/monasca-kibana-plugin.tar.gz - - ln -sf $pkg $easyPkg - - popd - fi -} - -function configure_kafka { - echo_summary "Configuring Kafka topics" - /opt/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 \ - --replication-factor 1 --partitions 4 --topic log - /opt/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 \ - --replication-factor 1 --partitions 4 --topic transformed-log -} - -function delete_kafka_topics { - echo_summary "Deleting Kafka topics" - /opt/kafka/bin/kafka-topics.sh --delete --zookeeper localhost:2181 \ - --topic log || true - /opt/kafka/bin/kafka-topics.sh --delete --zookeeper localhost:2181 \ - --topic transformed-log || true -} - -function create_log_management_accounts { - if is_service_enabled monasca-log-api; then - echo_summary "Enable Log Management in Keystone" - - # note(trebskit) following points to Kibana which is bad, - # but we do not have search-api in monasca-log-api now - # this code will be removed in future - local log_search_url="http://$KIBANA_SERVICE_HOST:$KIBANA_SERVICE_PORT/" - - get_or_create_service "logs" "logs" "Monasca Log service" - get_or_create_endpoint \ - "logs" \ - "$REGION_NAME" \ - "$MONASCA_LOG_API_URI_V3" \ - "$MONASCA_LOG_API_URI_V3" \ - "$MONASCA_LOG_API_URI_V3" - - get_or_create_service "logs-search" "logs-search" "Monasca Log search service" - get_or_create_endpoint \ - "logs-search" \ - "$REGION_NAME" \ - "$log_search_url" \ - "$log_search_url" \ - "$log_search_url" - - fi -} - -function enable_log_management { - if is_service_enabled horizon && is_service_enabled kibana; then - echo_summary "Configure Horizon with Kibana access" - - local localSettings=${DEST}/horizon/monitoring/config/local_settings.py - - sudo sed -e " - s|KIBANA_HOST = getattr(settings, 'KIBANA_HOST', 'http://192.168.10.4:5601/')|KIBANA_HOST = getattr(settings, 'KIBANA_HOST', 'http://${KIBANA_SERVICE_HOST}:${KIBANA_SERVICE_PORT}/')|g; - " -i ${localSettings} - - if is_service_enabled monasca-log-api; then - sudo sed -e " - s|'ENABLE_LOG_MANAGEMENT_BUTTON', False|'ENABLE_LOG_MANAGEMENT_BUTTON', True|g; - " -i ${localSettings} - fi - - restart_apache_server - fi -} - -function configure_tempest_for_monasca { - iniset $TEMPEST_CONFIG monitoring kibana_version $KIBANA_VERSION -} - -# check for service enabled -if is_service_enabled monasca-log; then - - if [[ "$1" == "stack" && "$2" == "pre-install" ]]; then - # Set up system services - echo_summary "Configuring Monasca Log Management system services" - pre_install - - elif [[ "$1" == "stack" && "$2" == "install" ]]; then - # Perform installation of service source - echo_summary "Installing Monasca Log Management" - install_monasca_log - - elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then - if is_service_enabled tempest; then - echo_summary "Configuring Tempest for Monasca" - configure_tempest_for_monasca - fi - - elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then - # Configure after the other layer 1 and 2 services have been configured - echo_summary "Configuring Monasca Log Management" - configure_monasca_log - - elif [[ "$1" == "stack" && "$2" == "extra" ]]; then - # Initialize and start the Monasca service - echo_summary "Initializing Monasca Log Management" - init_monasca_log - init_monasca_grafana_dashboards - if is_service_enabled monasca-agent; then - init_agent - fi - start_monasca_log - fi - - if [[ "$1" == "unstack" ]]; then - # Shut down Monasca services - echo_summary "Unstacking Monasca Log Management" - stop_monasca_log - delete_kafka_topics - fi - - if [[ "$1" == "clean" ]]; then - # Remove state and transient data - # Remember clean.sh first calls unstack.sh - echo_summary "Cleaning Monasca Log Management" - clean_monasca_log - fi -fi - -#Restore errexit -$_ERREXIT_LOG_API - -# Restore xtrace -$_XTRACE_LOG_API diff --git a/devstack/settings b/devstack/settings deleted file mode 100644 index adcfe789..00000000 --- a/devstack/settings +++ /dev/null @@ -1,60 +0,0 @@ -# -# Copyright 2016 FUJITSU LIMITED -# -# 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. -# - -# --- WARNING --- -# monasca-log-api DevStack plugin has been deprecated in Ussuri -# the code has been merged into monasca-api DevStack plugin -# the settings here apply only to stable/train and older branches -# please use settings in monasca-api for newer releases - -# services -enable_service zookeeper # devstack/monasca-api/zookeeper -enable_service kibana # devstack/monasca-api/kafka -enable_service elasticsearch -enable_service monasca-log -enable_service monasca-log-api -enable_service monasca-log-persister -enable_service monasca-log-transformer -enable_service monasca-log-metrics -enable_service monasca-log-agent - -# libraries/frameworks/projects monasca-log requires to install itself -KIBANA_VERSION=${KIBANA_VERSION:-4.6.3} -LOGSTASH_VERSION=${LOGSTASH_VERSION:-2.4.1} -ELASTICSEARCH_VERSION=${ELASTICSEARCH_VERSION:-2.4.6} -LOGSTASH_OUTPUT_MONASCA_VERSION=${LOGSTASH_OUTPUT_MONASCA_VERSION:-1.0.4} -NODE_JS_VERSION=${NODE_JS_VERSION:-"4.0.0"} -NVM_VERSION=${NVM_VERSION:-"0.33.5"} - -# repository settings -MONASCA_LOG_API_REPO=${MONASCA_LOG_API_REPO:-${GIT_BASE}/openstack/monasca-log-api.git} -MONASCA_LOG_API_BRANCH=${MONASCA_LOG_API_BRANCH:-master} -MONASCA_LOG_API_DIR=${DEST}/monasca-log-api - -MONASCA_COMMON_REPO=${MONASCA_COMMON_REPO:-${GIT_BASE}/openstack/monasca-common.git} -MONASCA_COMMON_BRANCH=${MONASCA_COMMON_BRANCH:-master} -MONASCA_COMMON_DIR=${DEST}/monasca-common - -MONASCA_STATSD_REPO=${MONASCA_STATSD_REPO:-${GIT_BASE}/openstack/monasca-statsd.git} -MONASCA_STATSD_BRANCH=${MONASCA_STATSD_BRANCH:-master} -MONASCA_STATSD_DIR=${DEST}/monasca-statsd - -MONASCA_KIBANA_PLUGIN_REPO=${MONASCA_KIBANA_PLUGIN_REPO:-${GIT_BASE}/openstack/monasca-kibana-plugin.git} -MONASCA_KIBANA_PLUGIN_BRANCH=${MONASCA_KIBANA_PLUGIN_BRANCH:-master} -MONASCA_KIBANA_PLUGIN_DIR=${DEST}/monasca-kibana-plugin - -DOWNLOAD_FILE_TIMEOUT=${DOWNLOAD_FILE_TIMEOUT:-300} diff --git a/doc/api_samples/v2/req_json.json b/doc/api_samples/v2/req_json.json deleted file mode 100644 index d70e2db3..00000000 --- a/doc/api_samples/v2/req_json.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message":"Hello World!" -} - diff --git a/doc/api_samples/v2/req_text.txt b/doc/api_samples/v2/req_text.txt deleted file mode 100644 index 557db03d..00000000 --- a/doc/api_samples/v2/req_text.txt +++ /dev/null @@ -1 +0,0 @@ -Hello World diff --git a/doc/api_samples/v3/req_global_dims.json b/doc/api_samples/v3/req_global_dims.json deleted file mode 100644 index b6735b68..00000000 --- a/doc/api_samples/v3/req_global_dims.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "dimensions":{ - "hostname":"mini-mon", - "service":"monitoring" - }, - "logs":[ - { - "message":"msg1", - "dimensions":{ - "component":"mysql", - "path":"/var/log/mysql.log" - } - }, - { - "message":"msg2", - "dimensions":{ - "component":"monasca-api", - "path":"/var/log/monasca/monasca-api.log" - } - } - ] -} diff --git a/doc/api_samples/v3/req_multiple_logs.json b/doc/api_samples/v3/req_multiple_logs.json deleted file mode 100644 index 4da99a16..00000000 --- a/doc/api_samples/v3/req_multiple_logs.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "dimensions":{}, - "logs":[ - { - "message":"msg1", - "dimensions":{ - "component":"mysql", - "path":"/var/log/mysql.log" - } - }, - { - "message":"msg2", - "dimensions":{ - "component":"monasca-api", - "path":"/var/log/monasca/monasca-api.log" - } - }, - { - "message":"msg3", - "dimensions":{ - "component":"monasca-log-api", - "path":"/var/log/monasca/monasca-log-api.log" - } - } - ] -} diff --git a/doc/api_samples/v3/req_single_log.json b/doc/api_samples/v3/req_single_log.json deleted file mode 100644 index 201ca18a..00000000 --- a/doc/api_samples/v3/req_single_log.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "dimensions":{}, - "logs":[ - { - "message":"msg1", - "dimensions":{ - "component":"mysql", - "path":"/var/log/mysql.log" - } - } - ] -} diff --git a/doc/source/.gitignore b/doc/source/.gitignore deleted file mode 100644 index 53dafec3..00000000 --- a/doc/source/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_static/*.sample diff --git a/doc/source/_static/.gitkeep b/doc/source/_static/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/doc/source/_static/images/architecture.png b/doc/source/_static/images/architecture.png deleted file mode 100644 index 7a8651fd46cd1337138eb3d5e30cbdd43898f5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 169988 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%0DMl-7;OZWB!r-dN4SV(?N4W~jnuLAH^ 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
  • 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 diff --git a/doc/source/_static/images/architecture.svg b/doc/source/_static/images/architecture.svg deleted file mode 100644 index e185e632..00000000 --- a/doc/source/_static/images/architecture.svg +++ /dev/null @@ -1,988 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Log API - - - - - - Elasticsearch - - - - - - store logs - - - - - - Message Queue - - - - - - publish log messages - - - - - - consume logs - - - - - - filter logspublish metrics - - - - - - parse logs - - - - - - UIKibana 4.4 - - - - - - Keystoneauthenticationplugin - - - - - - Log Agents - - - - - - Monasca outputplugin - - - - - - query logs - - - - - - POST logs - - - - - - Log Persister - - - - - - Log Metrics - - - - - - Log Transformer - - - - - - Logstash / Beaver - - - - - - Kafka - - - - - - IN: KafkaOUT: Kafka - - - - - - query logs (planned) - - - - - - IN: KafkaOUT: Kafka - - - - - - IN: KafkaOUT: Elasticsearch - - - - - - Logstash - - - - - - Logstash - - - - - - Logstash - - - - - - - - \ No newline at end of file diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst deleted file mode 100644 index 36a9908c..00000000 --- a/doc/source/admin/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -====================== - Administration guide -====================== - -.. toctree:: - :maxdepth: 2 diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst deleted file mode 100644 index 21354418..00000000 --- a/doc/source/cli/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -======================== - Command Line Interface -======================== - -At the moment, monasca-log-api cannot be operated -from the CLI. diff --git a/doc/source/conf.py b/doc/source/conf.py deleted file mode 100644 index a593242b..00000000 --- a/doc/source/conf.py +++ /dev/null @@ -1,281 +0,0 @@ -# -*- coding: utf-8 -*- -# -# monasca-log-api documentation build configuration file, created by -# sphinx-quickstart on Wed Nov 18 12:02:03 2015. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import os -import sys - -sys.path = [ - os.path.abspath('../..'), - os.path.abspath('../../bin') -] + sys.path - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.6' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.coverage', - 'sphinx.ext.ifconfig', - 'sphinx.ext.graphviz', - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'oslo_config.sphinxconfiggen', - 'oslo_config.sphinxext', - 'openstackdocstheme', - 'oslo_policy.sphinxpolicygen' -] - -# geeneral information about project -openstackdocs_repo_name = u'openstack/monasca-log-api' -openstackdocs_auto_name = False -project = u'Monasca Log Dev Docs' -openstackdocs_bug_project = u'monasca-log-api' -openstackdocs_bug_tag = u'doc' -copyright = u'2014-present, OpenStack Foundation' -author = u'OpenStack Foundation' - -# sample config -config_generator_config_file = [ - ('config-generator/monasca-log-api.conf', '_static/monasca-log-api') - -] -policy_generator_config_file = [ - ('config-generator/policy.conf', '_static/log-api') - -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [ - 'common', - 'doc', - 'documentation', - 'etc', - 'java' -] - -# If true, '()' will be appended to :func: etc. cross-reference text. -add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -show_authors = True - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'native' - -# A list of ignored prefixes for module index sorting. -modindex_common_prefix = ['monasca_log_api.', 'monasca'] - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'openstackdocs' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# doc. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# html_extra_path = [] - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -html_use_index = True - -# If false, no module index is generated. -html_use_modindex = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'monasca-log-apidoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'monasca-log-api.tex', u'monasca-log-api Documentation', - u'Openstack Foundation \\textless{}monasca@lists.launchpad.net\\textgreater{}', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'monasca-log-api', u'monasca-log-api Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'monasca-log-api', u'monasca-log-api Documentation', - author, 'monasca-log-api', 'Rest-API to collect logs from your cloud.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://doc.python.org/': None} diff --git a/doc/source/configuration/configuring.rst b/doc/source/configuration/configuring.rst deleted file mode 100644 index 4ef47417..00000000 --- a/doc/source/configuration/configuring.rst +++ /dev/null @@ -1,160 +0,0 @@ -.. _basic-configuration: - ------------ -Configuring ------------ - -monasca-log-api has several configuration options. Some of them -are inherited from oslo libraries, others can be found in the monasca-log-api -codebase. - -The entire configuration of monasca-log-api is defined in -configuration files. - -.. note:: This is intended behaviour. One of possible ways to deploy - monasca-log-api is to use **gunicorn**. Unfortunately gunicorn's - argument parsing clashes with oslo's argument parsing. - This means that gunicorn reports the CLI options of - oslo as unknown, and vice versa. - -There are 4 configuration files. For more details on the configuration -options, see :ref:`here `. - -Configuring Keystone Authorization ----------------------------------- - -Keystone authorization (i.e. verification of the token associated -with a request) is a critical part of monasca-log-api. -It prevents from unauthorized access and provides the isolation -needed for multi-tenancy. - -The configuration for ``keystonemiddleware`` should either be provided in -``monasca-log-api.conf`` or in a file in one of the configuration directories. -For more details about configuration options, check -`here `_. - -Configuring Log Publishing --------------------------- - -monasca-log-api sends all logs to the Kafka Message Queue. -Proper configuration should include: - -* ``kafka_url`` - comma-delimited list of Kafka brokers -* ``topics`` - names of the topics to which the logs will be pushed to -* ``max_message_size`` - maximum message size that can be posted a topic - -The configuration for ``log_publisher`` should either be provided in -``monasca-log-api.conf`` or in a file in one of the configuration directories. - -Configuring Healthcheck ------------------------ - -Healthcheck is an essential part of monasca-log-api. -It allows sending HTTP requests and getting knowledge about the -availability of the API. Configuration of healthcheck includes: - -* ``kafka_url`` - comma-delimited list of Kafka brokers -* ``kafka_topics`` - list of topics that existence is verified by healthcheck - -The configuration for ``kafka_healthcheck`` should either be provided in -``monasca-log-api.conf`` or in a file in one of the configuration directories. - -Configuring Monitoring ----------------------- - -monasca-log-api is capable of self-monitoring. This is achieved -through `monasca-statsd `_. -It assumes that there is monasca-agent available on the system node and -that statsd-server has been launched. - -There are several options you may want to tweak if necessary: - -* ``statsd_host``- the host statsd-server is bound to -* ``statsd_port``- the port statsd-server is bound to -* ``statsd_buffer`` - the amount of metrics to buffer in memory before sending - any -* ``dimensions`` - additional dimensions to be sent with all - metrics for this monasca-log-api instance - -The configuration for ``monitoring`` should either be provided in -``monasca-log-api.conf`` or in a file in one of the configuration directories. - -Configuring RBAC ----------------- - -The role-based access policy can be defined in the ``log-api.policy.yaml`` file -as described in `oslo.policy documentation -`_. - -Additionally, for historical reasons, custom RBAC mechanism is provided. It can -be configured as follows: - -* ``path`` - list of URIs that RBAC applies to -* ``default_roles`` - list of roles that are permitted to access the API -* ``agent_roles`` - list of roles, that if present, means that requests come - from log-agent -* ``delegate_roles`` - list of roles required by log-agent for sending logs - on behalf of another project (tenant) - -The configuration for ``roles_middleware`` can be provided either in -``monasca-log-api.conf`` or in a file in one of the configuration directories. - -Configuring Logging -------------------- - -Logging in monasca-log-api is controlled from the single -``log-api-logging.conf`` configuration file. -Here is a short list of several modifications you may want to apply, -based on your deployment: - -* to log INFO to console:: - - [handler_console] - level = INFO - -* to log DEBUG to file:: - - [handler_file] - level = DEBUG - -* to change the log file location:: - - [handler_file] - args = ('/var/log/log-api.log', 'a') - -* if you have an external script for log rotation:: - - [handler_file] - handler = logging.handlers.WatchedFileHandler - args = ('/var/log/log-api.log', 'a') - - That will store up to 5 rotations (each having maximum size - of 100MBs) - - The configuration of ``logging`` should be presented inside - ``log-api-logging.conf`` file and referenced from ``monasca-log-api.conf`` - using ``log_config_append`` option. - - If you want to know more about possible ways to save monasca-log-api logs, - feel free to visit: - - * `oslo.log `_ - * `Python HowTo `_ - * `Logging handlers `_ - -Configuring Policies --------------------- - -The policies for accessing each service can be configured in the -``log-api.policy.yaml`` configuration file:: - - Policy Description - Method Path - "Policy string": "Roles" - -example:: - - Logs post rule - POST /logs - POST /log/single - "log_api:logs:post": "role:monasca-user" diff --git a/doc/source/configuration/files.rst b/doc/source/configuration/files.rst deleted file mode 100644 index 1ca33d5b..00000000 --- a/doc/source/configuration/files.rst +++ /dev/null @@ -1,99 +0,0 @@ -.. _configuration-files: - -------------------- -Configuration files -------------------- - -Overview of monasca-log-api's configuration files. - -monasca-log-api.conf --------------------- - -This is the main configuration file of monasca-log-api. -It can be located in several places. During startup, -monasca-log-api searches for it in the following directories: - -* ``~/.monasca`` -* ``~/`` -* ``/etc/monasca`` -* ``/etc`` - -Alternatively, you can roll with a multi-file-based configuration model. -In this case, monasca-log-api searches the configuration files -in the following directories: - -* ``~/.monasca/monasca.conf.d/`` -* ``~/.monasca/monasca-log-api.conf.d/`` -* ``~/monasca.conf.d/`` -* ``~/monasca-log-api.conf.d/`` -* ``/etc/monasca/monasca.conf.d/`` -* ``/etc/monasca/monasca-log-api.conf.d/`` -* ``/etc/monasca.conf.d/`` -* ``/etc/monasca-log-api.conf.d/`` - -Regardless of the location, the name of the main configuration file -should always be ``monasca-log-api.conf``. For files located -in ``.conf.d`` directories, the name is irrelevant, but it should -indicate the file content. - -For example, when guring keystone communication. The -`keystonemiddleware `_ -configuration would be, therefore, located in, for example, -``/etc/monasca-log-api.conf.d/keystonemiddleware.conf`` - -A sample of this configuration file is also available -:ref:`here ` - -log-api-logging.conf --------------------- - -This file contains the logging setup for monasca-log-api. It should be -referenced from ``monasca-log-api.conf`` using, for example, -the following code snippet:: - - [DEFAULT] - log_config_append = /etc/monasca/log-api-logging.conf - -A sample of this configuration file is also available -:ref:`here ` - -log-api-paste.ini ------------------ - -This file contains the `PasteDeploy `_ -configuration. It describes all pipelines that are running within a single -instance of monasca-log-api. - -There is nothing you should try and modify in this file, -apart from enabling/disabling ``oslo_middleware.debug:Debug``. - -To enable ``oslo_middleware.debug:Debug`` for ``Log v3`` pipeline, -``log-api-paste.ini`` should contain code similar to this one:: - - [composite:main] - use = egg:Paste#urlmap - /v3.0: la_api_v3 - - [pipeline:la_api_v3] - pipeline = debug {{ other pipeline members }} - - [filter:debug] - paste.filter_factory = oslo_middleware.debug:Debug.factory - -This particular filter might be useful for examining the -WSGI environment during troubleshooting or local development. - -log-api.policy.yaml -------------------- - -This is the configuration file for policies to access the services. -the path of the file can be defined in ``monasca-log-api.conf``:: - - [oslo_policy] - policy_file = log-api.policy.yaml - -More information about policy file configuration can be found at -`oslo.policy `_ - -A sample of this configuration file is also available -:ref:`here ` diff --git a/doc/source/configuration/index.rst b/doc/source/configuration/index.rst deleted file mode 100644 index 180f2512..00000000 --- a/doc/source/configuration/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. _configuring: - -============= -Configuration -============= - -This section describes the configuration settings that can be specified. -Refer to :ref:`basic-configuration` for more details on the -available settings. - -.. toctree:: - :maxdepth: 1 - - configuring - files - options - sample diff --git a/doc/source/configuration/options.rst b/doc/source/configuration/options.rst deleted file mode 100644 index f7617956..00000000 --- a/doc/source/configuration/options.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _monasca-log-api.conf: - -------- -Options -------- - -.. show-options:: - :config-file: config-generator/monasca-log-api.conf diff --git a/doc/source/configuration/sample.rst b/doc/source/configuration/sample.rst deleted file mode 100644 index 4ee26c1a..00000000 --- a/doc/source/configuration/sample.rst +++ /dev/null @@ -1,51 +0,0 @@ -.. _sample-configuration: - -------- -Samples -------- - -The following sections show sample configuration files for monasca-log-api and -related utilities. These are generated from the code -(apart from the samples for logging and paster) and reflect the current state -of code in the monasca-log-api repository. - - -.. _sample-configuration-api: - -Sample Configuration For Application ------------------------------------- - -This sample configuration can also be viewed in `monasca-log-api.conf.sample -<../_static/monasca-log-api.conf.sample>`_. - -.. literalinclude:: ../_static/monasca-log-api.conf.sample - -.. _sample-configuration-logging: - -Sample Configuration For Logging --------------------------------- - -This sample configuration can also be viewed in `log-api-logging.conf -`_. - -.. literalinclude:: ../../../etc/monasca/log-api-logging.conf - - -Sample Configuration For Paste ------------------------------- - -This sample configuration can also be viewed in `log-api-paste.ini -`_. - -.. literalinclude:: ../../../etc/monasca/log-api-paste.ini - -.. _sample-configuration-policy: - -Sample Configuration For Policy -------------------------------- - -This sample configuration can also be viewed in `log-api-policy.yaml.sample -<../_static/log-api-policy.yaml.sample>`_. - -.. literalinclude:: ../_static/log-api.policy.yaml.sample - diff --git a/doc/source/contributor/.gitignore b/doc/source/contributor/.gitignore deleted file mode 100644 index d7f336d8..00000000 --- a/doc/source/contributor/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# codebase documentation is autogenerated -# so we do not want to track it in the tree -api/ diff --git a/doc/source/contributor/code.rst b/doc/source/contributor/code.rst deleted file mode 100644 index f8fa03f6..00000000 --- a/doc/source/contributor/code.rst +++ /dev/null @@ -1,18 +0,0 @@ -.. _codedocs: - -====================== -Codebase documentation -====================== - -Following section contains codebase documenation generated with, a little -bit of assistance, `sphinx.ext.autodoc`_. - -.. _`sphinx.ext.autodoc`: http://www.sphinx-doc.org/en/stable/ext/autodoc.html - -Modules -======= - -.. toctree:: - :maxdepth: 2 - - api/autoindex.rst diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst deleted file mode 100644 index 74449d29..00000000 --- a/doc/source/contributor/index.rst +++ /dev/null @@ -1,30 +0,0 @@ -======================= -Contribution Guidelines -======================= - -In the Contributions Guide, you will find documented policies for -developing with monasca-log. This includes the processes we use for -blueprints and specs, bugs, contributor onboarding, core reviewer -memberships, and other procedural items. - -monasca-log, as with all OpenStack projects, is written with the following -design guidelines in mind: - -* **Component based architecture**: Quickly add new behaviors -* **Highly available**: Scale to very serious workloads -* **Fault tolerant**: Isolated processes avoid cascading failures -* **Recoverable**: Failures should be easy to diagnose, debug, and rectify -* **Open standards**: Be a reference implementation for a community-driven api - -This documentation is generated by the Sphinx toolkit and lives in the source -tree. Additional documentation on monasca-log and other components of -OpenStack can be found on the `OpenStack wiki `_. - -Developer reference -------------------- - -.. toctree:: - :maxdepth: 1 - - tox - code diff --git a/doc/source/contributor/tox.rst b/doc/source/contributor/tox.rst deleted file mode 100644 index 3a47c7ab..00000000 --- a/doc/source/contributor/tox.rst +++ /dev/null @@ -1,77 +0,0 @@ -.. - monasca-log-api documentation master file - Copyright 2017 FUJITSU LIMITED - - 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. - -.. _`tox`: https://tox.readthedocs.io/en/latest/ -.. _`mandatory_tox_env`: https://github.com/openstack/monasca-log-api/blob/master/tox.ini#L2 - -=== -Tox -=== - -**monasca-log-api** uses `tox`_ to wrap up all the activities around -testing and linting the codebase. - -List of environments -==================== - -There is a rather large number of tox environments that **monasca-log-api** -is using. If necessary they can be enlisted with:: - - tox -a -v - -An output will be similar to this:: - - default environments: - py27 -> Runs unit test using Python2.7 - py35 -> Runs unit test using Python3.5 - pep8 -> Runs set of linters against codebase (flake8, bandit, - bashate, checkniceness) - cover -> Calculates code coverage - - additional environments: - api-guide -> Called from CI scripts to test and publish the API Guide - api-ref -> Called from CI scripts to test and publish the API Ref - apidocs -> Generates codebase documentation - bandit -> [no description] - bashate -> Validates (pep8-like) devstack plugins - checkjson -> Validates all json samples inside doc folder - checkniceness -> Validates (pep-like) documenation - debug -> Allows to run unit-test with debug mode enabled - docs -> Builds api-ref, api-guide, releasenotes and doc - flake8 -> [no description] - releasenotes -> Called from CI script to test and publish the Release Notes - venv -> [no description] - - -Running tox -=========== - -Running tox is as simple as:: - - tox - -That will run all **mandatory** (for details refer to `mandatory_tox_env`_) -environments. Having them passed is *a must have*. - -Running specific environments -============================= - -If you require to run specific environments, please use:: - - tox -e api-ref,api-guide,releasenotes - -Result of which will be having all documentations sub-components generated -and ready in local dev environment. diff --git a/doc/source/deprecation_note.inc b/doc/source/deprecation_note.inc deleted file mode 100644 index 3169cb92..00000000 --- a/doc/source/deprecation_note.inc +++ /dev/null @@ -1,6 +0,0 @@ -.. note:: The Log API v2 has been DEPRECATED in the Mitaka release. The - migration path is to use the `Log API v3 - `_ instead of version 2 - of the API. The Log API v2 will ultimately be removed, following the - `OpenStack standard deprecation policy - `_. diff --git a/doc/source/glossary.rst b/doc/source/glossary.rst deleted file mode 100644 index fa106d19..00000000 --- a/doc/source/glossary.rst +++ /dev/null @@ -1,3 +0,0 @@ -======== -Glossary -======== diff --git a/doc/source/index.rst b/doc/source/index.rst deleted file mode 100644 index 9f060eaf..00000000 --- a/doc/source/index.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. - monasca-log-api documentation master file - Copyright 2016-2017 FUJITSU LIMITED - - 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. - -=========================================== -Welcome to monasca-log-api's documentation! -=========================================== - -monasca-log-api is a RESTful API server acting as gateway for -logs collected from log-agents. - -.. include:: deprecation_note.inc - -The developer documentation provided here is continually kept up-to-date -based on the latest code, and may not represent the state of the project at -any specific prior release. - -.. note:: This is documentation for developers, if you are looking for more - general documentation including API, install, operator and user - guides see `docs.openstack.org`_ - -.. _`docs.openstack.org`: https://docs.openstack.org - -.. toctree:: - :maxdepth: 2 - - user/index - admin/index - install/index - configuration/index - cli/index - contributor/index - -.. toctree:: - :maxdepth: 1 - - glossary - diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst deleted file mode 100644 index e6bc440e..00000000 --- a/doc/source/install/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -============== - Installation -============== - -.. toctree:: - :maxdepth: 2 diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst deleted file mode 100644 index 475c57c9..00000000 --- a/doc/source/user/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -============ - User guide -============ - -.. toctree:: - :maxdepth: 2 diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index efe1f5ba..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,57 +0,0 @@ -ARG DOCKER_IMAGE=monasca/log-api -ARG APP_REPO=https://review.opendev.org/openstack/monasca-log-api - -# Branch, tag or git hash to build from. -ARG REPO_VERSION=master -ARG CONSTRAINTS_BRANCH=master - -# Extra Python3 dependencies. -ARG EXTRA_DEPS="gunicorn python-memcached gevent" - -# Always start from `monasca-base` image and use specific tag of it. -ARG BASE_TAG=master -FROM monasca/base:$BASE_TAG - -# Environment variables used for our service or wait scripts. -ENV \ - KAFKA_URI=kafka:9092 \ - KAFKA_WAIT_FOR_TOPICS=log \ - MONASCA_CONTAINER_LOG_API_PORT=5607 \ - MEMCACHED_URI=memcached:11211 \ - AUTHORIZED_ROLES=admin,domainuser,domainadmin,monasca-user \ - AGENT_AUTHORIZED_ROLES=monasca-agent \ - KEYSTONE_IDENTITY_URI=http://keystone:35357 \ - KEYSTONE_AUTH_URI=http://keystone:5000 \ - KEYSTONE_ADMIN_USER=admin \ - KEYSTONE_ADMIN_PASSWORD=secretadmin \ - KEYSTONE_ADMIN_TENANT=admin \ - KEYSTONE_ADMIN_DOMAIN=default \ - GUNICORN_WORKERS=9 \ - GUNICORN_WORKER_CLASS=gevent \ - GUNICORN_WORKER_CONNECTIONS=2000 \ - GUNICORN_BACKLOG=1000 \ - GUNICORN_TIMEOUT=10 \ - PYTHONIOENCODING=utf-8 \ - ADD_ACCESS_LOG=false \ - ACCESS_LOG_FORMAT="%(asctime)s [%(process)d] gunicorn.access [%(levelname)s] %(message)s" \ - ACCESS_LOG_FIELDS='%(h)s %(l)s %(u)s %(t)s %(r)s %(s)s %(b)s "%(f)s" "%(a)s" %(L)s' \ - LOG_LEVEL_ROOT=INFO \ - LOG_LEVEL_CONSOLE=INFO \ - LOG_LEVEL_ACCESS=INFO \ - STAY_ALIVE_ON_FAILURE="false" - -# Copy all neccessary files to proper locations. -COPY log-api* monasca-log-api* /etc/monasca/ - -# Run here all additionals steps your service need post installation. -# Stay with only one `RUN` and use `&& \` for next steps to don't create -# unnecessary image layers. Clean at the end to conserve space. -#RUN \ -# echo "Some steps to do after main installation." && \ -# echo "Hello when building." - -# Expose port for specific service. -EXPOSE ${MONASCA_CONTAINER_LOG_API_PORT} - -# Implement start script in `start.sh` file. -CMD ["/start.sh"] diff --git a/docker/README.rst b/docker/README.rst deleted file mode 100644 index d00a73fc..00000000 --- a/docker/README.rst +++ /dev/null @@ -1,85 +0,0 @@ -================================ -Docker image for Monasca Log API -================================ -The Monasca log API image is based on the monasca-base image. - - -Building monasca-base image -=========================== -See https://github.com/openstack/monasca-common/tree/master/docker/README.rst - - -Building Monasca log API image -============================== - -Example: - $ ./build_image.sh - - -Requirements from monasca-base image -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -health_check.py - This file will be used for checking the status of the Monasca Log API - application. - - -Scripts -~~~~~~~ -start.sh - In this starting script provide all steps that lead to the proper service - start. Including usage of wait scripts and templating of configuration - files. You also could provide the ability to allow running container after - service died for easier debugging. - -build_image.sh - Please read detailed build description inside the script. - - -Environment variables -~~~~~~~~~~~~~~~~~~~~~ -============================== ======================================================================= ========================================== -Variable Default Description -============================== ======================================================================= ========================================== -KAFKA_URI kafka:9092 URI to Apache Kafka (distributed streaming platform) -KAFKA_WAIT_FOR_TOPICS log The topic where log-api streams the log messages -KAFKA_WAIT_RETRIES 24 Number of kafka connect attempts -KAFKA_WAIT_DELAY 5 Seconds to wait between attempts -MONASCA_CONTAINER_LOG_API_PORT 5607 The port from the log pipeline endpoint -MEMCACHED_URI memcached:11211 URI to Keystone authentication cache -AUTHORIZED_ROLES admin,domainuser,domainadmin,monasca-user Roles for Monasca users (full API access) -AGENT_AUTHORIZED_ROLES monasca-agent Roles for Monasca agents (sending data only) -KEYSTONE_IDENTITY_URI http://keystone:35357 URI to Keystone admin endpoint -KEYSTONE_AUTH_URI http://keystone:5000 URI to Keystone public endpoint -KEYSTONE_ADMIN_USER admin OpenStack administrator user name -KEYSTONE_ADMIN_PASSWORD secretadmin OpenStack administrator user password -KEYSTONE_ADMIN_TENANT admin OpenStack administrator tenant name -KEYSTONE_ADMIN_DOMAIN default OpenStack administrator domain -GUNICORN_WORKERS 9 Number of gunicorn (WSGI-HTTP server) workers -GUNICORN_WORKER_CLASS gevent Used gunicorn worker class -GUNICORN_WORKER_CONNECTIONS 2000 Number of gunicorn worker connections -GUNICORN_BACKLOG 1000 Number of gunicorn backlogs -GUNICORN_TIMEOUT 10 Gunicorn connection timeout -PYTHONIOENCODING utf-8 Python encoding -ADD_ACCESS_LOG false Enable gunicorn request/access logging -ACCESS_LOG_FORMAT "%(asctime)s [%(process)d] gunicorn.access [%(levelname)s] %(message)s" Define the logging format -ACCESS_LOG_FIELDS '%(h)s %(l)s %(u)s %(t)s %(r)s %(s)s %(b)s "%(f)s" "%(a)s" %(L)s' Define the fields to be logged -LOG_LEVEL_ROOT WARN Log level for root logging -LOG_LEVEL_CONSOLE INFO Log level for console logging -LOG_LEVEL_ACCESS INFO Log level for access logging -STAY_ALIVE_ON_FAILURE false If true, container runs 2 hours after tests fail -============================== ======================================================================= ========================================== - - -Provide configuration templates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* monasca-log-api.conf.j2 -* log-api-gunicorn.conf.j2 -* log-api-logging.conf.j2 -* log-api.paste.ini.j2 - - -Links -~~~~~ -https://docs.openstack.org/monasca-log-api/latest/configuration/ - -https://github.com/openstack/monasca-log-api/blob/master/README.rst diff --git a/docker/build_image.sh b/docker/build_image.sh deleted file mode 100755 index 9e15bc02..00000000 --- a/docker/build_image.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/bin/bash - -# 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. - -# TODO(Dobroslaw): move this script to monasca-common/docker folder -# and leave here small script to download it and execute using env variables -# to minimize code duplication. - -set -x # Print each script step. -set -eo pipefail # Exit the script if any statement returns error. - -# This script is used for building Docker image with proper labels -# and proper version of monasca-common. -# -# Example usage: -# $ ./build_image.sh -# -# Everything after `./build_image.sh` is optional and by default configured -# to get versions from `Dockerfile`. -# -# To build from master branch (default): -# $ ./build_image.sh -# To build specific version run this script in the following way: -# $ ./build_image.sh stable/queens -# Building from specific commit: -# $ ./build_image.sh cb7f226 -# When building from a tag monasca-common will be used in version available -# in upper constraint file: -# $ ./build_image.sh 2.5.0 -# To build image from Gerrit patch sets that is targeting branch stable/queens: -# $ ./build_image.sh refs/changes/51/558751/1 stable/queens -# -# If you want to build image with custom monasca-common version you need -# to provide it as in the following example: -# $ ./build_image.sh master master refs/changes/19/595719/3 - -# Go to folder with Docker files. -REAL_PATH=$(python -c "import os,sys; print(os.path.realpath('$0'))") -cd "$(dirname "$REAL_PATH")/../docker/" - -[ -z "$DOCKER_IMAGE" ] && \ - DOCKER_IMAGE=$(\grep DOCKER_IMAGE Dockerfile | cut -f2 -d"=") - -: "${REPO_VERSION:=$1}" -[ -z "$REPO_VERSION" ] && \ - REPO_VERSION=$(\grep REPO_VERSION Dockerfile | cut -f2 -d"=") -# Let's stick to more readable version and disable SC2001 here. -# shellcheck disable=SC2001 -REPO_VERSION_CLEAN=$(echo "$REPO_VERSION" | sed 's|/|-|g') - -[ -z "$APP_REPO" ] && APP_REPO=$(\grep APP_REPO Dockerfile | cut -f2 -d"=") -GITHUB_REPO=$(echo "$APP_REPO" | sed 's/review.opendev.org/github.com/' | \ - sed 's/ssh:/https:/') - -if [ -z "$CONSTRAINTS_FILE" ]; then - CONSTRAINTS_FILE=$(\grep CONSTRAINTS_FILE Dockerfile | cut -f2 -d"=") || true - : "${CONSTRAINTS_FILE:=https://releases.openstack.org/constraints/upper/master}" -fi - -: "${CONSTRAINTS_BRANCH:=$2}" -[ -z "$CONSTRAINTS_BRANCH" ] && \ - CONSTRAINTS_BRANCH=$(\grep CONSTRAINTS_BRANCH Dockerfile | cut -f2 -d"=") - -# When using stable version of repository use same stable constraints file. -case "$REPO_VERSION" in - *stable*) - CONSTRAINTS_BRANCH_CLEAN="$REPO_VERSION" - CONSTRAINTS_FILE=${CONSTRAINTS_FILE/master/$CONSTRAINTS_BRANCH_CLEAN} - # Get monasca-common version from stable upper constraints file. - CONSTRAINTS_TMP_FILE=$(mktemp) - wget --output-document "$CONSTRAINTS_TMP_FILE" \ - $CONSTRAINTS_FILE - UPPER_COMMON=$(\grep 'monasca-common' "$CONSTRAINTS_TMP_FILE") - # Get only version part from monasca-common. - UPPER_COMMON_VERSION="${UPPER_COMMON##*===}" - rm -rf "$CONSTRAINTS_TMP_FILE" - ;; - *) - CONSTRAINTS_BRANCH_CLEAN="$CONSTRAINTS_BRANCH" - ;; -esac - -# Monasca-common variables. -if [ -z "$COMMON_REPO" ]; then - COMMON_REPO=$(\grep COMMON_REPO Dockerfile | cut -f2 -d"=") || true - : "${COMMON_REPO:=https://review.opendev.org/openstack/monasca-common}" -fi -: "${COMMON_VERSION:=$3}" -if [ -z "$COMMON_VERSION" ]; then - COMMON_VERSION=$(\grep COMMON_VERSION Dockerfile | cut -f2 -d"=") || true - if [ "$UPPER_COMMON_VERSION" ]; then - # Common from upper constraints file. - COMMON_VERSION="$UPPER_COMMON_VERSION" - fi -fi - -# Clone project to temporary directory for getting proper commit number from -# branches and tags. We need this for setting proper image labels. -# Docker does not allow to get any data from inside of system when building -# image. -TMP_DIR=$(mktemp -d) -( - cd "$TMP_DIR" - # This many steps are needed to support gerrit patch sets. - git init - git remote add origin "$APP_REPO" - git fetch origin "$REPO_VERSION" - git reset --hard FETCH_HEAD -) -GIT_COMMIT=$(git -C "$TMP_DIR" rev-parse HEAD) -[ -z "${GIT_COMMIT}" ] && echo "No git commit hash found" && exit 1 -rm -rf "$TMP_DIR" - -# Do the same for monasca-common. -COMMON_TMP_DIR=$(mktemp -d) -( - cd "$COMMON_TMP_DIR" - # This many steps are needed to support gerrit patch sets. - git init - git remote add origin "$COMMON_REPO" - git fetch origin "$COMMON_VERSION" - git reset --hard FETCH_HEAD -) -COMMON_GIT_COMMIT=$(git -C "$COMMON_TMP_DIR" rev-parse HEAD) -[ -z "${COMMON_GIT_COMMIT}" ] && echo "No git commit hash found" && exit 1 -rm -rf "$COMMON_TMP_DIR" - -CREATION_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - -docker build --no-cache \ - --build-arg CREATION_TIME="$CREATION_TIME" \ - --build-arg GITHUB_REPO="$GITHUB_REPO" \ - --build-arg APP_REPO="$APP_REPO" \ - --build-arg REPO_VERSION="$REPO_VERSION" \ - --build-arg GIT_COMMIT="$GIT_COMMIT" \ - --build-arg CONSTRAINTS_FILE="$CONSTRAINTS_FILE" \ - --build-arg COMMON_REPO="$COMMON_REPO" \ - --build-arg COMMON_VERSION="$COMMON_VERSION" \ - --build-arg COMMON_GIT_COMMIT="$COMMON_GIT_COMMIT" \ - --tag "$DOCKER_IMAGE":"$REPO_VERSION_CLEAN" . diff --git a/docker/health_check.py b/docker/health_check.py deleted file mode 100755 index d190cd3e..00000000 --- a/docker/health_check.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 - -# (C) Copyright 2018 FUJITSU LIMITED -# -# 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. - -"""Health check will returns 0 when service is working properly.""" - -import logging -import os -import sys -from urllib import request - - -LOG_LEVEL = logging.getLevelName(os.environ.get('LOG_LEVEL', 'INFO')) -logging.basicConfig(level=LOG_LEVEL) -logger = logging.getLogger(__name__) - -API_PORT = os.environ.get('MONASCA_CONTAINER_LOG_API_PORT', '5607') -url = "http://localhost:" + API_PORT + "/healthcheck" - - -def main(): - """Send health check request to health check endpoint of log API.""" - logger.debug('Send health check request to %s', url) - try: - request.urlopen(url=url) - except Exception as ex: - logger.error('Exception during request handling: ' + repr(ex)) - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/docker/log-api-gunicorn.conf.j2 b/docker/log-api-gunicorn.conf.j2 deleted file mode 100644 index d81e2526..00000000 --- a/docker/log-api-gunicorn.conf.j2 +++ /dev/null @@ -1,13 +0,0 @@ -bind = '0.0.0.0:{{ MONASCA_CONTAINER_LOG_API_PORT }}' -proc_name = 'monasca-log-api' - -backlog = {{ GUNICORN_BACKLOG | int }} -workers = {{ GUNICORN_WORKERS | int }} -worker_class = '{{ GUNICORN_WORKER_CLASS }}' -timeout = {{ GUNICORN_TIMEOUT | int }} - -{% if ADD_ACCESS_LOG == true %} -accesslog = '-' -access_log_format = '{{ ACCESS_LOG_FIELDS }}' -{% endif %} -capture_output = True diff --git a/docker/log-api-logging.conf.j2 b/docker/log-api-logging.conf.j2 deleted file mode 100644 index 76d60f16..00000000 --- a/docker/log-api-logging.conf.j2 +++ /dev/null @@ -1,47 +0,0 @@ -[default] -disable_existing_loggers = 0 - -[loggers] -keys = root, gunicorn_access, kafka - -[handlers] -keys = console, gunicorn_access - -[formatters] -keys = context, gunicorn_access - -[logger_root] -level = {{ LOG_LEVEL_ROOT }} -handlers = console - -[logger_gunicorn_access] -level = {{ LOG_LEVEL_ACCESS }} -handlers = console -propagate = 0 -qualname = gunicorn.access - -[logger_kafka] -qualname = kafka -level = DEBUG -handlers = console -propagate = 0 - -[handler_console] -class = logging.StreamHandler -args = (sys.stdout,) -level = {{ LOG_LEVEL_CONSOLE }} -formatter = context - -[handler_gunicorn_access] -class = logging.StreamHandler -args = (sys.stdout,) -level = {{ LOG_LEVEL_ACCESS }} -formatter = gunicorn_access - -[formatter_context] -class = oslo_log.formatters.ContextFormatter - -[formatter_gunicorn_access] -class = logging.Formatter -format = {{ ACCESS_LOG_FORMAT }} -datefmt = %Y-%m-%d %H:%M:%S diff --git a/docker/log-api-paste.ini.j2 b/docker/log-api-paste.ini.j2 deleted file mode 100644 index 05ac128a..00000000 --- a/docker/log-api-paste.ini.j2 +++ /dev/null @@ -1,53 +0,0 @@ -[DEFAULT] -name = monasca_log_api - -[composite:main] -use = egg:Paste#urlmap -/: la_version -/healthcheck: la_healthcheck -/v2.0: la_api_v2 -/v3.0: la_api_v3 - -[pipeline:la_version] -pipeline = error_trap versionapp - -[pipeline:la_healthcheck] -pipeline = error_trap healthcheckapp - -[pipeline:la_api_v2] -pipeline = error_trap request_id auth roles api_v2_app - -[pipeline:la_api_v3] -pipeline = error_trap request_id auth roles api_v3_app - -[app:versionapp] -paste.app_factory = monasca_log_api.app.api:create_version_app - -[app:healthcheckapp] -paste.app_factory = monasca_log_api.app.api:create_healthcheck_app - -[app:api_v2_app] -paste.app_factory = monasca_log_api.app.api:create_api_app -set api_version=v2.0 - -[app:api_v3_app] -paste.app_factory = monasca_log_api.app.api:create_api_app -set api_version=v3.0 - -[filter:auth] -paste.filter_factory = keystonemiddleware.auth_token:filter_factory - -[filter:roles] -paste.filter_factory = monasca_log_api.middleware.role_middleware:RoleMiddleware.factory - -[filter:request_id] -paste.filter_factory = oslo_middleware.request_id:RequestId.factory - -[filter:debug] -paste.filter_factory = oslo_middleware.debug:Debug.factory - -[filter:error_trap] -paste.filter_factory = oslo_middleware.catch_errors:CatchErrors.factory - -[server:main] -use = egg:gunicorn#main diff --git a/docker/monasca-log-api.conf.j2 b/docker/monasca-log-api.conf.j2 deleted file mode 100644 index 5ae94610..00000000 --- a/docker/monasca-log-api.conf.j2 +++ /dev/null @@ -1,42 +0,0 @@ -[DEFAULT] -log_config_append=/etc/monasca/log-api-logging.conf - -[monitoring] -enable = {{ MONITORING_ENABLE | default(False) }} -statsd_host = {{ STATSD_HOST | default('127.0.0.1') }} -statsd_port = {{ STATSD_PORT | default(8125) }} -statsd_buffer = {{ STATSD_BUFFER | default(50) }} - -[service] -region = useast -max_log_size = 1048576 - -[roles_middleware] -path = /v2.0/log,/v3.0/logs -default_roles = {{ AUTHORIZED_ROLES | default('admin, domainuser, domainadmin, monasca-user') }} -agent_roles = {{ AGENT_AUTHORIZED_ROLES | default('monasca-agent') }} - -[log_publisher] -topics = log -kafka_url = {{ KAFKA_URI | default('kafka:9092') }} -max_message_size = 1048576 - -[kafka_healthcheck] -kafka_url = {{ KAFKA_URI | default('kafka:9092') }} -kafka_topics = log - -[keystone_authtoken] -auth_type = password -auth_url = {{ KEYSTONE_IDENTITY_URI }} -auth_uri = {{ KEYSTONE_AUTH_URI }} -username = {{ KEYSTONE_ADMIN_USER }} -password = {{ KEYSTONE_ADMIN_PASSWORD }} -user_domain_name = Default -project_name = {{ KEYSTONE_ADMIN_TENANT }} -project_domain_name = Default -service_token_roles_required = true -memcached_servers = {{ MEMCACHED_URI }} -insecure = false -cafile = -certfile = -keyfile = diff --git a/docker/start.sh b/docker/start.sh deleted file mode 100644 index 26d92d81..00000000 --- a/docker/start.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/sh - -# 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. - -# Starting script. -# All checks and configuration templating you need to do before service -# could be safely started should be added in this file. - -set -eo pipefail # Exit the script if any statement returns error. - -# Test services we need before starting our service. -echo "Start script: waiting for needed services" -python3 /kafka_wait_for_topics.py - -# Template all config files before start, it will use env variables. -# Read usage examples: https://pypi.org/project/Templer/ -echo "Start script: creating config files from templates" -templer -v -f /etc/monasca/monasca-log-api.conf.j2 /etc/monasca/monasca-log-api.conf -templer -v -f /etc/monasca/log-api-gunicorn.conf.j2 /etc/monasca/log-api-gunicorn.conf -templer -v -f /etc/monasca/log-api-logging.conf.j2 /etc/monasca/log-api-logging.conf -templer -v -f /etc/monasca/log-api-paste.ini.j2 /etc/monasca/log-api-paste.ini - -# Start our service. -# gunicorn --args -echo "Start script: starting container" -gunicorn \ - --config /etc/monasca/log-api-gunicorn.conf \ - --paste /etc/monasca/log-api-paste.ini - -# Allow server to stay alive in case of failure for 2 hours for debugging. -RESULT=$? -if [ $RESULT != 0 ] && [ "$STAY_ALIVE_ON_FAILURE" = "true" ]; then - echo "Service died, waiting 120 min before exiting" - sleep 7200 -fi -exit $RESULT diff --git a/documentation/monasca-log-api-kafka.md b/documentation/monasca-log-api-kafka.md deleted file mode 100644 index 615bfc7b..00000000 --- a/documentation/monasca-log-api-kafka.md +++ /dev/null @@ -1,142 +0,0 @@ -# Monasca Log API - Kafka - -Date: April 18, 2016 - -Document Version: v0.2 - -## Introduction - -**monasca-log-api** uses kafka transport to ship received logs down to the -processing pipeline. - -For more information about Kafka, please see [official documentation] -(http://kafka.apache.org/documentation.html). - -## Output message format -Messages sent to kafka should have following format -(top level object is called **envelope**) and is combined out of three -elements: -* log -* creation_time -* meta - - - "log": , - "creation_time": , - "meta": - -Log property should have at least following form: - - "message": , - "dimensions": - -Meta property should have following form: - - "tenantId": , - "region": - -Full example as json: -```json - { - "log": { - "message": "2015-11-13 12:44:42.411 27297 DEBUG kafka [-] Read 31/31 bytes from Kafka _read_bytes /opt/monasca/monasca-log-api/lib/python2.7/site-packages/kafka/conn.py:103", - "dimensions": { - "hostname": "devstack" - } - }, - "creation_time": 1447834886, - "meta": { - "tenantId": "e4bd29509eda473092d32aadfee3e7b1", - "region": "pl" - } - } -``` - -### Fields explanation - -* log - contains log specific information collected from the system. In the -most lean case that would be: **message**, **dimensions** - * message - normally that represent a single line from a log file - * dimensions - informations such as hostname where application is running -* creation_time - UNIX timestamp representing moment when log message was created -by monasca-log-api -* meta - contains tenantId and its region - -**log** entry may of course contain many more fields that are considered valid -in given case. However two mentioned in this documentation are required. - -All fields, apart from **creation_time** and **log**, are created from HTTP headers. -Description is available [here](/documentation/monasca-log-api-spec.md). - -## Truncating too large message - - Following section mostly applies to monasca-log-api v3.0 - -Each *envelope* sent to Kafka is serialized into JSON string. This string must -comply to Kafka limitation about [maximum message size](https://kafka.apache.org/08/configuration.html). -If JSON message is too big following actions are taken -1) difference between maximum allowed size and JSON message size (both in bytes). - ```diff = (size(json_envelope) + size(envelope_key) + KAFKA_METADATA_SIZE) - maximum_allowed_size + TRUNCATION_SAFE_OFFSET```. - **KAFKA_METADATA_SIZE** is amount of bytes Kafka adds during transformation - of each message prior to sending it -2) log is enriched with property **truncated** set to **true** (```log['truncated'] = True```) -3) log's message is truncated by ```diff + TRUNCATED_PROPERTY_SIZE```. - **TRUNCATED_PROPERTY_SIZE** is the size of newly added property. - -Variables explanation: - -* **envelope_key** is the key used when routing logs into specific kafka partitions. -Its byte size is always fixed (determined from the byte size of timestamp represented as string). -```len(bytearray(str(int(time.time() * 1000)).encode('utf-8')))``` -* **KAFKA_METADATA_SIZE** equals to 200 bytes. -* **TRUNCATION_SAFE_OFFSET** is equal to 1 ensuring that diff size will be always positive number -* **TRUNCATED_PROPERTY_SIZE** is calculated as byte size of expression ```log['truncated'] = True``` -for each run of log-api. - -## Configuration - -### Java - -Configuration for kafka should be placed in *.yml file and look similar to: -```yml -logTopic: logs -kafka: - brokerUris: - - localhost:8900 - zookeeperUris: - - localhost:2181 - healthCheckTopic: healthcheck -``` - -It is composed out of two relevant pieces -* logTopic - topic where data should be sent -* kafka - section containing information required to communicate in kafka. -For more details see [here](https://github.com/openstack/monasca-common/blob/master/java/monasca-common-kafka/src/main/java/monasca/common/messaging/kafka/KafkaConfiguration.java) - -### Python - -Configuration for kafka should be placed in *.conf file and look similar to: - -```conf -[log_publisher] -topics = 'logs' -kafka_url = 'localhost:8900' -``` - -There are only two relevant options: -* topics - comma delimited list of topics where data should be sent -* kafka_url - address where kafka server is running - - # Copyright 2016-2017 FUJITSU LIMITED - # - # 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. diff --git a/documentation/monasca-log-api-metrics.md b/documentation/monasca-log-api-metrics.md deleted file mode 100644 index fbe44bf9..00000000 --- a/documentation/monasca-log-api-metrics.md +++ /dev/null @@ -1,108 +0,0 @@ -# Monitoring for monasca-log-api - -**monasca-log-api** can be monitored by examining following metrics - -| Name | Meaning | Dimensions | -|-------------------------------------------|-------------------------|---------------| -| monasca.log.in_logs | Amount of received logs | version | -| monasca.log.in_logs_rejected | Amount of rejected logs (see below for details) | version | -| monasca.log.in_bulks_rejected | Amount of rejected bulks (see below for details) | version | -| monasca.log.in_logs_bytes | Size received logs (a.k.a. *Content-Length)* in bytes | version | -| monasca.log.out_logs | Amount of logs published to kafka | | -| monasca.log.out_logs_lost | Amount of logs lost during publish phase | | -| monasca.log.out_logs_truncated_bytes | Amount of truncated bytes, removed from message | | -| monasca.log.publish_time_ms | Time Log-Api needed to publish all logs to kafka | | -| monasca.log.processing_time_ms | Time Log-Api needed to process received logs. | version | - -Additionally each metric contains following dimensions: -- **component** - monasca-log-api -- **service** - monitoring - -## Metrics explained - -### monasca.log.in_logs - -Metric sent with amount of logs that were received by **Log-API** -and successfully passed initial validation. For **v2.0** this -metric will be always send with value one, for **v3.0** this metric's values -are equivalent to amount of element in bulk request. - -### monasca.log.in_logs_rejected - -Logs can be rejected because of: - -* checking content-type -* checking content-length -* reading payload -* retrieving logs from payload -* validating global dimensions (if set)(only valid for v3.0) - -### monasca.log.in_bulks_rejected (only v3.0) - -In **v2.0** bulk request is equivalent to single request (i.e. single-element bulk). -However in **v3.0** rejecting logs is done in two phases. - -*Phase 1* is when there is no way to determine actual amount of logs -that were sent by client (see [monasca.log.in_logs_rejected](#monasca_log_logs_rejected)) -If any of these steps was impossible to be executed entire bulk is -considered lost and thus all logs within. - -If *Phase 1* passes, *Phase 2* is executed. At this point every -piece of data is available, however still some logs can be rejected, -because of: - -* lack of certain fields (i.e. message) -* invalid local dimensions (if set) - -In *Phase 2* metric [monasca.log.in_logs_rejected](#monasca_log_logs_rejected) -is produced. - -### monasca.log.in_logs_bytes - -Metric allows to track to size of requests API receives. -In **v3.0** To simplify implementation it is equivalent to **Content-Length** value. -However amount of global dimensions and other metadata when compared -to size of logs is negligible. - -### monasca.log.out_logs - -Amount of logs successfully published to kafka queue. - -### monasca.log.out_logs_lost - -Amount of logs that were not sent to kafka and **Log-API** was unable -to recover from error situation - -### monasca.log.out_logs_truncated_bytes - -Metric is sent with the amount of bytes that log's message is shorten -by if necessary. To read more about truncation see [here](/documentation/monasca-log-api-kafka.md). -If truncation did not happen, which should be normal situation for most -of the time, metric is updated with value **0**. - -### monasca.log.publish_time_ms - -Time that was needed to send all the logs into all the topics. -*monasca.log.processing_time_ms* includes value of that metric -within. It exists to see how much does publishing take in entire -processing. - -### monasca.log.processing_time_ms - -Total amount of time logs spent inside **Log-API**. Metric does not -include time needed to communicate with Keystone to authenticate request. -As far as possible it is meant to track **Log-API** itself - - # Copyright 2016-2017 FUJITSU LIMITED - # - # 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. diff --git a/documentation/monasca-log-api-spec.md b/documentation/monasca-log-api-spec.md deleted file mode 100644 index 165675cc..00000000 --- a/documentation/monasca-log-api-spec.md +++ /dev/null @@ -1,319 +0,0 @@ -# Monasca Log API - -Date: May 27, 2016 - -Document Version: v2.2.2 - -# Logs -The logs resource allows logs to be created and queried. - -## Create Logs -Create logs. - -### POST /v3.0/logs - -#### Headers -* X-Auth-Token (string, required) - Keystone auth token -* Content-Type (string, required) - application/json - -#### Path Parameters -None. - -#### Query Parameters -* tenant_id (string, optional, restricted) - Tenant ID (project ID) to create - log on behalf of. Usage of this query parameter requires the role specified - in the configuration option `delegate_roles` . - -#### Request Body -JSON object which can have a maximum size of 5 MB. It consists of global -dimensions (optional) and array of logs. Each single log message with -resulting envelope can have a maximum size of 1 MB. -Dimensions is a dictionary of key-value pairs and should be consistent with -metric dimensions. - -Logs is an array of JSON objects describing the log entries. Every log object -can have individual set of dimensions which has higher precedence than global -ones. It should be noted that dimensions presented in each log record are also -optional. - - If both global (at the root level) and local (at log entry level) - dimensions would be present, they will be merged into one dictionary. - Please note that local dimensions are logically considered as more - specific thus in case of conflict (i.e. having two entries with the same - key in both global and local dimensions) local dimensions take - precedence over global dimensions. - -#### Request Examples - -POST logs - -``` -POST /v3.0/logs HTTP/1.1 -Host: 192.168.10.4:5607 -Content-Type: application/json -X-Auth-Token: 27feed73a0ce4138934e30d619b415b0 -Cache-Control: no-cache - -{ - "dimensions":{ - "hostname":"mini-mon", - "service":"monitoring" - }, - "logs":[ - { - "message":"msg1", - "dimensions":{ - "component":"mysql", - "path":"/var/log/mysql.log" - } - }, - { - "message":"msg2", - "dimensions":{ - "component":"monasca-api", - "path":"/var/log/monasca/monasca-api.log" - } - } - ] -} -``` - -### Response -#### Status Code -* 204 - No content - -#### Response Body -This request does not return a response body. - -## List logs -Get precise log listing filtered by dimensions. - - Note that this API is in development, and is not currently implemented. - -This interface can used to obtain log entries for a time range, based on -matching a set of exact dimension values. By default, entries will be returned -in descending timestamp order (newest first). The log entries returned by this -API will not necessarily be identical to those POST-ed to the service, as the -data returned will have been subjected to deployment-specific transformation -stages (i.e. the "monasca-log-transform" service). - -### GET /v3.0/logs - -#### Headers -* X-Auth-Token (string, required) - Keystone auth token -* Accept (string) - application/json - -#### Path Parameters -None. - -#### Query Parameters -* tenant_id (string, optional, restricted) - Tenant ID from which to get logs -from. This parameter can be used to get logs from a tenant other than the tenant -the request auth token is scoped to. Usage of this query parameter is restricted -to users with the monasca admin role, as defined in the monasca-log-api -configuration file, which defaults to `monasca-admin`. -* dimensions (string, optional) - A dictionary to filter logs by specified as a -comma separated array of (key, value) pairs as `key1:value1,key2:value2, ...`, -multiple values for a key may be specified as -`key1:value1|value2|...,key2:value4,...`. If the value is omitted in the form -`key1,key2, ...`, then entries are returned where the dimension exists with -any value. -* start_time (string, optional) - The start time in ISO 8601 combined date and -time format in UTC. -* end_time (string, optional) - The end time in ISO 8601 combined date and time -format in UTC. -* offset (integer, optional) - Number of log entries to skip (Default: 0). -* limit (integer, optional) - Limit number of logs returned (Default: 10). -* sort_by (string, optional) - Comma separated list of fields or dimensions to -sort by. Fields may be followed by 'asc' or 'desc' to set the direction, e.g. -'timestamp asc'. Allowed fields for sort_by are currently: 'timestamp'. -(Default: no sorting) - -#### Request Body -None. - -#### Request Examples -``` -GET /v3.0/logs?dimensions=hostname:devstack&start_time=2015-03-00T00:00:01Z HTTP/1.1 -Host: 192.168.10.4:5607 -Content-Type: application/json -X-Auth-Token: 2b8882ba2ec44295bf300aecb2caa4f7 -Cache-Control: no-cache -``` - -### Response -#### Status Code -* 200 - OK - -#### Response Body -Returns a JSON object with a 'links' array of links and an 'elements' array of -log entry objects with the following fields: - -* timestamp (timestamp) - The originating time in ISO 8601 combined date and -time format in UTC, with millisecond resolution if available. -* message (string) - The contents of the log message. -* dimensions ({string(255): string(255)}) - Dimensions of the log, either -supplied with the log or added by transformation. - -#### Response Examples -``` -{ - "links": [ - { - "rel": "prev", - "href": "http://192.168.10.4:5607/v3.0/logs?start_time=2015-03-00T00%3A00%3A00Z&dimensions=hostname%3Adevstack" - }, - { - "rel": "self", - "href": "http://192.168.10.4:5607/v3.0/logs?offset=10&start_time=2015-03-00T00%3A00%3A00Z&dimensions=hostname%3Adevstack" - }, - { - "rel": "next", - "href": "http://192.168.10.4:5607/v3.0/logs?offset=20&start_time=2015-03-00T00%3A00%3A00Z&dimensions=hostname%3Adevstack" - } - ], - "elements": [ - { - "timestamp":"2015-03-03T05:24:55.202Z", - "message":"msg1", - "dimensions":{ - "hostname":"devstack", - "component":"mysql", - "path":"/var/log/mysql.log" - } - }, - { - "timestamp":"2015-03-01T02:22:09.112Z", - "message":"msg2", - "dimensions":{ - "hostname":"devstack", - "component":"monasca-api", - "path":"/var/log/monasca/monasca-api.log" - } - } - ] -} -``` - - -# Healthcheck - - Note that following part is updated for Python implementation. - -The *Monasca Log API* comes with a built-in health check mechanism. -It is available in two flavors, both accessible under ```/healthcheck``` -endpoint. - -## Complex check -The complex check not only returns a response with success code if *Monasca Log API* -is up and running but it also verifies if peripheral components, such as **Kafka**, -are healthy too. - -*Monasca Log API* will respond with following codes: - -* 200 - both API and external components are healthy. -* 503 - API is running but problems with peripheral components have been spotted. - -Example: -```curl -XGET 192.168.10.4:5607/healthcheck``` - -### Peripheral checks - -* **Kafka** is considered healthy if connection to broker can be established -and configured topics can be found. - -## Simple check -The simple check only returns response only if *Monasca Log API* is up and running. -It does not return any data because it is accessible only for ```HEAD``` requests. -If the *Monasca Log API* is running the following response code: ```204``` is expected. - -Example: -```curl -XHEAD 192.168.10.4:5607/healthcheck``` - - -======= -### POST /v2.0/log/single (deprecated) - -#### Headers -* X-Auth-Token (string, required) - Keystone auth token -* Content-Type (string, required) - application/json; text/plain -* X-Application-Type (string(255), optional) - Type of application -* X-Dimensions ({string(255):string(255)}, required) - A dictionary consisting of (key, value) pairs used to structure logs. - -#### Path Parameters -None. - -#### Request Body -Consists of a single plain text message or a JSON object which can have a maximum length of 1048576 characters. - -#### Request Examples - -##### Plain text log - single line -POST a single line of plain text log. - -``` -POST /v2.0/log/single HTTP/1.1 -Host: 192.168.10.4:5607 -Content-Type: text/plain -X-Auth-Token: 27feed73a0ce4138934e30d619b415b0 -X-Application-Type: apache -X-Dimensions: applicationname:WebServer01,environment:production -Cache-Control: no-cache - -Hello World -``` - -##### Plain text log - multi lines -POST a multiple lines of plain text log. - -``` -POST /v2.0/log/single HTTP/1.1 -Host: 192.168.10.4:5607 -Content-Type: text/plain -X-Auth-Token: 27feed73a0ce4138934e30d619b415b0 -X-Application-Type: apache -X-Dimensions: applicationname:WebServer01,environment:production -Cache-Control: no-cache - -Hello\nWorld -``` - -##### JSON log -POST a JSON log - -``` -POST /v2.0/log/single HTTP/1.1 -Host: 192.168.10.4:5607 -Content-Type: application/json -X-Auth-Token: 27feed73a0ce4138934e30d619b415b0 -X-Application-Type: apache -X-Dimensions: applicationname:WebServer01,environment:production -Cache-Control: no-cache - -{ - "message":"Hello World!", - "from":"hoover" -} - -``` - -### Response -#### Status Code -* 204 - No content - -#### Response Body -This request does not return a response body. - - # Copyright 2016-2017 FUJITSU LIMITED - # - # 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. diff --git a/etc/monasca/log-api-logging.conf b/etc/monasca/log-api-logging.conf deleted file mode 100644 index 74d02ea1..00000000 --- a/etc/monasca/log-api-logging.conf +++ /dev/null @@ -1,34 +0,0 @@ -[loggers] -keys = root, kafka - -[handlers] -keys = console, file - -[formatters] -keys = context - -[logger_root] -level = INFO -handlers = console, file - -[logger_kafka] -qualname = kafka -level = INFO -handlers = console, file -propagate = 0 - -[handler_console] -class = logging.StreamHandler -args = (sys.stderr,) -level = DEBUG -formatter = context - -[handler_file] -class = logging.handlers.RotatingFileHandler -level = DEBUG -formatter = context -# store up to 5*100MB of logs -args = ('/var/log/monasca/log-api.log', 'a', 104857600, 5) - -[formatter_context] -class = oslo_log.formatters.ContextFormatter diff --git a/etc/monasca/log-api-paste.ini b/etc/monasca/log-api-paste.ini deleted file mode 100644 index 0732a87c..00000000 --- a/etc/monasca/log-api-paste.ini +++ /dev/null @@ -1,78 +0,0 @@ -# -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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. -# - -[DEFAULT] -name = main - -[composite:main] -use = egg:Paste#urlmap -/: la_version -/healthcheck: la_healthcheck -/v2.0: la_api_v2 -/v3.0: la_api_v3 - -[pipeline:la_version] -pipeline = error_trap versionapp - -[pipeline:la_healthcheck] -pipeline = error_trap healthcheckapp - -[pipeline:la_api_v2] -pipeline = error_trap request_id auth api_v2_app - -[pipeline:la_api_v3] -pipeline = error_trap request_id auth api_v3_app - -[app:versionapp] -paste.app_factory = monasca_log_api.app.api:create_version_app - -[app:healthcheckapp] -paste.app_factory = monasca_log_api.app.api:create_healthcheck_app - -[app:api_v2_app] -paste.app_factory = monasca_log_api.app.api:create_api_app -set api_version=v2.0 - -[app:api_v3_app] -paste.app_factory = monasca_log_api.app.api:create_api_app -set api_version=v3.0 - -[filter:auth] -paste.filter_factory = keystonemiddleware.auth_token:filter_factory - -[filter:request_id] -paste.filter_factory = oslo_middleware.request_id:RequestId.factory - -# NOTE(trebskit) this is optional -# insert this into either pipeline to get some WSGI environment debug output -[filter:debug] -paste.filter_factory = oslo_middleware.debug:Debug.factory - -[filter:error_trap] -paste.filter_factory = oslo_middleware.catch_errors:CatchErrors.factory - -[server:main] -use = egg:gunicorn#main -bind = 127.0.0.1:5607 -workers = 9 -worker-connections = 2000 -worker-class = eventlet -timeout = 30 -backlog = 2048 -keepalive = 2 -proc_name = monasca-log-api -loglevel = DEBUG diff --git a/etc/monasca/log-api-uwsgi.ini b/etc/monasca/log-api-uwsgi.ini deleted file mode 100644 index 49d56191..00000000 --- a/etc/monasca/log-api-uwsgi.ini +++ /dev/null @@ -1,25 +0,0 @@ -[uwsgi] -wsgi-file = /usr/local/bin/monasca-log-api-wsgi - -# Versions of mod_proxy_uwsgi>=2.0.6 should use a UNIX socket, see -# http://uwsgi-docs.readthedocs.org/en/latest/Apache.html#mod-proxy-uwsgi -uwsgi-socket = 127.0.0.1:5607 - -# Override the default size for headers from the 4k default. -buffer-size = 65535 - -# This is running standalone -master = true - -enable-threads = true - -# Tune this to your environment. -processes = 4 - -# uwsgi recommends this to prevent thundering herd on accept. -thunder-lock = true - -plugins = python - -# This ensures that file descriptors aren't shared between keystone processes. -lazy-apps = true diff --git a/lower-constraints.txt b/lower-constraints.txt deleted file mode 100644 index 160607d7..00000000 --- a/lower-constraints.txt +++ /dev/null @@ -1,87 +0,0 @@ -alabaster==0.7.10 -appdirs==1.3.0 -Babel==2.3.4 -bandit==1.1.0 -bashate==0.5.1 -chardet==3.0.4 -configparser==3.5.0 -coverage==4.0 -debtcollector==1.2.0 -doc8==0.6.0 -docutils==0.11 -dulwich==0.15.0 -eventlet==0.18.2 -extras==1.0.0 -falcon==2.0.0 -fixtures==3.0.0 -future==0.16.0 -gevent==1.2.2 -gitdb==0.6.4 -GitPython==1.0.1 -greenlet==0.4.10 -imagesize==0.7.1 -iso8601==0.1.11 -Jinja2==2.10 -kazoo==2.2 -keystoneauth1==3.4.0 -keystonemiddleware==4.17.0 -linecache2==1.0.0 -MarkupSafe==1.0 -mccabe==0.2.1 -monasca-common==2.7.0 -monasca-statsd==1.1.0 -monotonic==0.6 -mox3==0.20.0 -msgpack-python==0.4.0 -netaddr==0.7.18 -netifaces==0.10.4 -openstackdocstheme==2.2.1 -os-api-ref==1.5.0 -os-client-config==1.28.0 -os-testr==1.0.0 -oslo.config==5.2.0 -oslo.context==2.19.2 -oslo.i18n==3.15.3 -oslo.log==3.36.0 -oslo.middleware==3.31.0 -oslo.policy==1.30.0 -oslo.serialization==2.18.0 -oslo.utils==3.33.0 -oslotest==3.2.0 -Paste==2.0.2 -PasteDeploy==1.5.0 -pbr==2.0.0 -positional==1.2.1 -pycadf==1.1.0 -pyflakes==0.8.1 -Pygments==2.2.0 -pyinotify==0.9.6 -PyMySQL==0.7.6 -pyparsing==2.1.0 -python-dateutil==2.5.3 -python-keystoneclient==3.8.0 -python-mimeparse==1.6.0 -python-statsd==2.1.0 -python-subunit==1.0.0 -pytz==2013.6 -PyYAML==3.12 -reno==3.1.0 -requests==2.14.2 -requestsexceptions==1.2.0 -restructuredtext-lint==1.1.1 -rfc3986==0.3.1 -simplejson==3.8.1 -six==1.10.0 -smmap==0.9.0 -snowballstemmer==1.2.1 -Sphinx==2.0.0 -sphinxcontrib-websupport==1.0.1 -statsd==3.2.1 -stestr==1.0.0 -stevedore==1.20.0 -tabulate==0.8.1 -testtools==2.2.0 -traceback2==1.4.0 -unittest2==1.1.0 -WebOb==1.7.1 -wrapt==1.7.0 diff --git a/monasca_log_api/__init__.py b/monasca_log_api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/app/__init__.py b/monasca_log_api/app/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/app/api.py b/monasca_log_api/app/api.py deleted file mode 100644 index 6505d0bd..00000000 --- a/monasca_log_api/app/api.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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. - -""" -Module contains factories to initializes various applications -of monasca-log-api -""" - -import six - -import falcon -from oslo_log import log - -from monasca_log_api.app.base import error_handlers -from monasca_log_api.app.base import request -from monasca_log_api.app.controller import healthchecks -from monasca_log_api.app.controller.v2 import logs as v2_logs -from monasca_log_api.app.controller.v3 import logs as v3_logs -from monasca_log_api.app.controller import versions -from monasca_log_api import config - - -def error_trap(app_name): - """Decorator trapping any error during application boot time""" - - @six.wraps(error_trap) - def _wrapper(func): - - @six.wraps(_wrapper) - def _inner_wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except Exception: - logger = log.getLogger(__name__) - logger.exception('Failed to load application \'%s\'', app_name) - raise - - return _inner_wrapper - - return _wrapper - - -def singleton_config(func): - """Decorator ensuring that configuration is loaded only once.""" - - @six.wraps(singleton_config) - def _wrapper(global_config, **local_conf): - config.parse_args() - return func(global_config, **local_conf) - - return _wrapper - - -@error_trap('version') -def create_version_app(global_conf, **local_conf): - """Creates Version application""" - - ctrl = versions.Versions() - controllers = { - '/': ctrl, # redirect http://host:port/ down to Version app - # avoid conflicts with actual pipelines and 404 error - '/version': ctrl, # list all the versions - '/version/{version_id}': ctrl # display details of the version - } - - wsgi_app = falcon.API( - request_type=request.Request - ) - wsgi_app.req_options.strip_url_path_trailing_slash = True - for route, ctrl in controllers.items(): - wsgi_app.add_route(route, ctrl) - return wsgi_app - - -@error_trap('healthcheck') -def create_healthcheck_app(global_conf, **local_conf): - """Creates Healthcheck application""" - - ctrl = healthchecks.HealthChecks() - controllers = { - '/': ctrl - } - - wsgi_app = falcon.API( - request_type=request.Request - ) - for route, ctrl in controllers.items(): - wsgi_app.add_route(route, ctrl) - return wsgi_app - - -@error_trap('api') -@singleton_config -def create_api_app(global_conf, **local_conf): - """Creates MainAPI application""" - - controllers = {} - api_version = global_conf.get('api_version') - - if api_version == 'v2.0': - controllers.update({ - '/log/single': v2_logs.Logs() - }) - elif api_version == 'v3.0': - controllers.update({ - '/logs': v3_logs.Logs() - }) - - wsgi_app = falcon.API( - request_type=request.Request - ) - - for route, ctrl in controllers.items(): - wsgi_app.add_route(route, ctrl) - - error_handlers.register_error_handlers(wsgi_app) - - return wsgi_app diff --git a/monasca_log_api/app/base/__init__.py b/monasca_log_api/app/base/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/app/base/error_handlers.py b/monasca_log_api/app/base/error_handlers.py deleted file mode 100644 index db986511..00000000 --- a/monasca_log_api/app/base/error_handlers.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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 falcon - -from monasca_log_api.app.base import model - - -def log_envelope_exception_handler(ex, req, resp, params): - raise falcon.HTTPUnprocessableEntity( - title='Failed to create Envelope', - description=ex.message) - - -def register_error_handlers(app): - app.add_error_handler(model.LogEnvelopeException, - log_envelope_exception_handler) diff --git a/monasca_log_api/app/base/exceptions.py b/monasca_log_api/app/base/exceptions.py deleted file mode 100644 index 2886c0b6..00000000 --- a/monasca_log_api/app/base/exceptions.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2015 FUJITSU LIMITED -# -# 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 falcon - - -HTTP_422 = '422 Unprocessable Entity' - - -class HTTPUnprocessableEntity(falcon.OptionalRepresentation, falcon.HTTPError): - """HTTPUnprocessableEntity http error. - - HTTPError that comes with '422 Unprocessable Entity' status - - :argument: message(str) - meaningful description of what caused an error - :argument: kwargs - any other option defined in - :py:class:`falcon.OptionalRepresentation` and - :py:class:`falcon.HTTPError` - """ - def __init__(self, message, **kwargs): - falcon.HTTPError.__init__(self, - HTTP_422, - 'unprocessable_entity', - message, - **kwargs - ) diff --git a/monasca_log_api/app/base/log_publisher.py b/monasca_log_api/app/base/log_publisher.py deleted file mode 100644 index 1394b1df..00000000 --- a/monasca_log_api/app/base/log_publisher.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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 time - -import falcon -from monasca_common.kafka import producer -from monasca_log_api.common.rest import utils as rest_utils -from oslo_log import log -from oslo_utils import encodeutils - -from monasca_log_api.app.base import model -from monasca_log_api import conf -from monasca_log_api.monitoring import client -from monasca_log_api.monitoring import metrics - -LOG = log.getLogger(__name__) -CONF = conf.CONF - -_RETRY_AFTER = 60 -_TIMESTAMP_KEY_SIZE = len( - bytearray(str(int(time.time() * 1000)).encode('utf-8'))) -_TRUNCATED_PROPERTY_SIZE = len( - bytearray('"truncated": true'.encode('utf-8'))) -_KAFKA_META_DATA_SIZE = 32 -_TRUNCATION_SAFE_OFFSET = 1 - - -class InvalidMessageException(Exception): - pass - - -class LogPublisher(object): - """Publishes log data to Kafka - - LogPublisher is able to send single message to multiple configured topic. - It uses following configuration written in conf file :: - - [log_publisher] - topics = 'logs' - kafka_url = 'localhost:8900' - - Note: - Uses :py:class:`monasca_common.kafka.producer.KafkaProducer` - to ship logs to kafka. For more details - see `monasca_common`_ github repository. - - .. _monasca_common: https://github.com/openstack/monasca-common - - """ - - def __init__(self): - self._topics = CONF.log_publisher.topics - self.max_message_size = CONF.log_publisher.max_message_size - - self._kafka_publisher = producer.KafkaProducer( - url=CONF.log_publisher.kafka_url - ) - if CONF.monitoring.enable: - self._statsd = client.get_client() - - # setup counter, gauges etc - self._logs_published_counter = self._statsd.get_counter( - metrics.LOGS_PUBLISHED_METRIC - ) - self._publish_time_ms = self._statsd.get_timer( - metrics.LOGS_PUBLISH_TIME_METRIC - ) - self._logs_lost_counter = self._statsd.get_counter( - metrics.LOGS_PUBLISHED_LOST_METRIC - ) - self._logs_truncated_gauge = self._statsd.get_gauge( - metrics.LOGS_TRUNCATED_METRIC - ) - - LOG.info('Initializing LogPublisher <%s>', self) - - def send_message(self, messages): - """Sends message to each configured topic. - - Note: - Falsy messages (i.e. empty) are not shipped to kafka - - See also - * :py:class:`monasca_log_api.common.model.Envelope` - * :py:meth:`._is_message_valid` - - :param dict|list messages: instance (or instances) of log envelope - """ - - if not messages: - return - if not isinstance(messages, list): - messages = [messages] - - sent_counter = 0 - num_of_msgs = len(messages) - - LOG.debug('About to publish %d messages to %s topics', - num_of_msgs, self._topics) - - try: - send_messages = [] - - for message in messages: - msg = self._transform_message(message) - send_messages.append(msg) - if CONF.monitoring.enable: - with self._publish_time_ms.time(name=None): - self._publish(send_messages) - else: - self._publish(send_messages) - - sent_counter = len(send_messages) - except Exception as ex: - LOG.exception('Failure in publishing messages to kafka') - raise ex - finally: - self._after_publish(sent_counter, num_of_msgs) - - def _transform_message(self, message): - """Transforms message into JSON. - - Method executes transformation operation for - single element. Operation is set of following - operations: - - * checking if message is valid - (:py:func:`.LogPublisher._is_message_valid`) - * truncating message if necessary - (:py:func:`.LogPublisher._truncate`) - - :param model.Envelope message: instance of message - :return: serialized message - :rtype: str - """ - if not self._is_message_valid(message): - raise InvalidMessageException() - truncated = self._truncate(message) - return encodeutils.safe_encode(truncated, incoming='utf-8') - - def _truncate(self, envelope): - """Truncates the message if needed. - - Each message send to kafka is verified. - Method checks if message serialized to json - exceeds maximum allowed size that can be posted to kafka - queue. If so, method truncates message property of the log - by difference between message and allowed size. - - :param Envelope envelope: original envelope - :return: serialized message - :rtype: str - """ - - msg_str = model.serialize_envelope(envelope) - envelope_size = ((len(bytearray(msg_str, 'utf-8', 'replace')) + - _TIMESTAMP_KEY_SIZE + - _KAFKA_META_DATA_SIZE) - if msg_str is not None else -1) - - diff_size = ((envelope_size - self.max_message_size) + - _TRUNCATION_SAFE_OFFSET) - - if diff_size > 1: - truncated_by = diff_size + _TRUNCATED_PROPERTY_SIZE - - LOG.warning(('Detected message that exceeds %d bytes,' - 'message will be truncated by %d bytes'), - self.max_message_size, - truncated_by) - - log_msg = envelope['log']['message'] - truncated_log_msg = log_msg[:-truncated_by] - - envelope['log']['truncated'] = True - envelope['log']['message'] = truncated_log_msg - if CONF.monitoring.enable: - self._logs_truncated_gauge.send(name=None, value=truncated_by) - - msg_str = rest_utils.as_json(envelope) - else: - if CONF.monitoring.enable: - self._logs_truncated_gauge.send(name=None, value=0) - - return msg_str - - def _publish(self, messages): - """Publishes messages to kafka. - - :param list messages: list of messages - """ - num_of_msg = len(messages) - - LOG.debug('Publishing %d messages', num_of_msg) - - try: - for topic in self._topics: - self._kafka_publisher.publish( - topic, - messages - ) - LOG.debug('Sent %d messages to topic %s', num_of_msg, topic) - except Exception as ex: - raise falcon.HTTPServiceUnavailable('Service unavailable', - str(ex), 60) - - @staticmethod - def _is_message_valid(message): - """Validates message before sending. - - Methods checks if message is :py:class:`model.Envelope`. - By being instance of this class it is ensured that all required - keys are found and they will have their values. - - """ - return message and isinstance(message, model.Envelope) - - def _after_publish(self, send_count, to_send_count): - """Executed after publishing to sent metrics. - - :param int send_count: how many messages have been sent - :param int to_send_count: how many messages should be sent - - """ - - failed_to_send = to_send_count - send_count - - if failed_to_send == 0: - LOG.debug('Successfully published all [%d] messages', - send_count) - else: - error_str = ('Failed to send all messages, %d ' - 'messages out of %d have not been published') - LOG.error(error_str, failed_to_send, to_send_count) - if CONF.monitoring.enable: - self._logs_published_counter.increment(value=send_count) - self._logs_lost_counter.increment(value=failed_to_send) diff --git a/monasca_log_api/app/base/model.py b/monasca_log_api/app/base/model.py deleted file mode 100644 index 0acc78ac..00000000 --- a/monasca_log_api/app/base/model.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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 oslo_utils import timeutils -import six - -from monasca_log_api.common.rest import utils as rest_utils - - -def serialize_envelope(envelope): - """Returns json representation of an envelope. - - :return: json object of envelope - :rtype: six.text_type - - """ - json = rest_utils.as_json(envelope, ensure_ascii=False) - - if six.PY2: - raw = six.text_type(json.replace(r'\\', r'\\\\'), encoding='utf-8', - errors='replace') - else: - raw = json - - return raw - - -class LogEnvelopeException(Exception): - pass - - -class Envelope(dict): - def __init__(self, log, meta): - if not log: - error_msg = 'Envelope cannot be created without log' - raise LogEnvelopeException(error_msg) - if 'tenantId' not in meta or not meta.get('tenantId'): - error_msg = 'Envelope cannot be created without tenant' - raise LogEnvelopeException(error_msg) - - creation_time = self._get_creation_time() - super(Envelope, self).__init__( - log=log, - creation_time=creation_time, - meta=meta - ) - - @staticmethod - def _get_creation_time(): - return timeutils.utcnow_ts() - - @classmethod - def new_envelope(cls, log, tenant_id, region, dimensions=None): - """Creates new log envelope - - Log envelope is combined ouf of following properties - - * log - dict - * creation_time - timestamp - * meta - meta block - - Example output json would like this: - - .. code-block:: json - - { - "log": { - "message": "Some message", - "dimensions": { - "hostname": "devstack" - } - }, - "creation_time": 1447834886, - "meta": { - "tenantId": "e4bd29509eda473092d32aadfee3e7b1", - "region": "pl" - } - } - - :param dict log: original log element (containing message and other - params - :param str tenant_id: tenant id to be put in meta field - :param str region: region to be put in meta field - :param dict dimensions: additional dimensions to be appended to log - object dimensions - - """ - if dimensions: - log['dimensions'].update(dimensions) - - log_meta = { - 'region': region, - 'tenantId': tenant_id - } - - return cls(log, log_meta) - - @property - def log(self): - return self.get('log', None) - - @property - def creation_time(self): - return self.get('creation_time', None) - - @property - def meta(self): - return self.get('meta', None) diff --git a/monasca_log_api/app/base/request.py b/monasca_log_api/app/base/request.py deleted file mode 100644 index 11d6a97d..00000000 --- a/monasca_log_api/app/base/request.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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 falcon - -from monasca_common.policy import policy_engine as policy - -from monasca_log_api.app.base import request_context -from monasca_log_api.app.base import validation -from monasca_log_api import policies - -policy.POLICIES = policies - - -_TENANT_ID_PARAM = 'tenant_id' -"""Name of the query-param pointing at project-id (tenant-id)""" - - -class Request(falcon.Request): - """Variation of falcon.Request with context - - Following class enhances :py:class:`falcon.Request` with - :py:class:`context.RequestContext`. - - """ - - def __init__(self, env, options=None): - super(Request, self).__init__(env, options) - self.context = request_context.RequestContext.from_environ(self.env) - - def validate(self, content_types): - """Performs common request validation - - Validation checklist (in that order): - - * :py:func:`validation.validate_content_type` - * :py:func:`validation.validate_payload_size` - * :py:func:`validation.validate_cross_tenant` - - :param content_types: allowed content-types handler supports - :type content_types: list - :raises Exception: if any of the validation fails - - """ - validation.validate_content_type(self, content_types) - validation.validate_payload_size(self) - validation.validate_cross_tenant( - tenant_id=self.project_id, - roles=self.roles, - cross_tenant_id=self.cross_project_id - ) - - @property - def project_id(self): - """Returns project-id (tenant-id) - - :return: project-id - :rtype: str - - """ - return self.context.project_id - - @property - def cross_project_id(self): - """Returns project-id (tenant-id) found in query params. - - This particular project-id is later on identified as - cross-project-id - - :return: project-id - :rtype: str - - """ - return self.get_param(_TENANT_ID_PARAM, required=False) - - @property - def user_id(self): - """Returns user-id - - :return: user-id - :rtype: str - - """ - return self.context.user - - @property - def roles(self): - """Returns roles associated with user - - :return: user's roles - :rtype: list - - """ - return self.context.roles - - def can(self, action, target=None): - return self.context.can(action, target) - - def __repr__(self): - return '%s, context=%s' % (self.path, self.context) diff --git a/monasca_log_api/app/base/request_context.py b/monasca_log_api/app/base/request_context.py deleted file mode 100644 index 5805c40d..00000000 --- a/monasca_log_api/app/base/request_context.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# Copyright 2018 OP5 AB -# -# 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 monasca_common.policy import policy_engine as policy -from oslo_context import context - -from monasca_log_api import policies - -policy.POLICIES = policies - - -class RequestContext(context.RequestContext): - """RequestContext. - - RequestContext is customized version of - :py:class:oslo_context.context.RequestContext. - """ - - def can(self, action, target=None): - if target is None: - target = {'project_id': self.project_id, - 'user_id': self.user_id} - - return policy.authorize(self, action=action, target=target) diff --git a/monasca_log_api/app/base/validation.py b/monasca_log_api/app/base/validation.py deleted file mode 100644 index 872625e6..00000000 --- a/monasca_log_api/app/base/validation.py +++ /dev/null @@ -1,267 +0,0 @@ -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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 re - -import falcon -from oslo_log import log -import six - -from monasca_log_api.app.base import exceptions -from monasca_log_api import conf - -LOG = log.getLogger(__name__) -CONF = conf.CONF - -APPLICATION_TYPE_CONSTRAINTS = { - 'MAX_LENGTH': 255, - 'PATTERN': re.compile('^[a-zA-Z0-9_.\\-]+$') -} -"""Application type constraint used in validation. - -See :py:func:`Validations.validate_application_type` -""" -DIMENSION_NAME_CONSTRAINTS = { - 'MAX_LENGTH': 255, - 'PATTERN': re.compile('[^><={}(), \'";&]+$') -} -"""Constraint for name of single dimension. - -See :py:func:`Validations.validate_dimensions` -""" -DIMENSION_VALUE_CONSTRAINTS = { - 'MAX_LENGTH': 255 -} -"""Constraint for value of single dimension. - -See :py:func:`Validations.validate_dimensions` -""" - - -def validate_application_type(application_type=None): - """Validates application type. - - Validation won't take place if application_type is None. - For details see: :py:data:`APPLICATION_TYPE_CONSTRAINTS` - - :param str application_type: application type - """ - - def validate_length(): - if (len(application_type) > - APPLICATION_TYPE_CONSTRAINTS['MAX_LENGTH']): - msg = ('Application type {type} must be ' - '{length} characters or less') - raise exceptions.HTTPUnprocessableEntity( - msg.format( - type=application_type, - length=APPLICATION_TYPE_CONSTRAINTS[ - 'MAX_LENGTH'] - ) - ) - - def validate_match(): - if (not APPLICATION_TYPE_CONSTRAINTS['PATTERN'] - .match(application_type)): - raise exceptions.HTTPUnprocessableEntity( - 'Application type %s may only contain: "a-z A-Z 0-9 _ - ."' - % application_type - ) - - if application_type: - validate_length() - validate_match() - - -def _validate_dimension_name(name): - try: - if len(name) > DIMENSION_NAME_CONSTRAINTS['MAX_LENGTH']: - raise exceptions.HTTPUnprocessableEntity( - 'Dimension name %s must be 255 characters or less' % - name - ) - if name[0] == '_': - raise exceptions.HTTPUnprocessableEntity( - 'Dimension name %s cannot start with underscore (_)' % - name - ) - if not DIMENSION_NAME_CONSTRAINTS['PATTERN'].match(name): - raise exceptions.HTTPUnprocessableEntity( - 'Dimension name %s may not contain: %s' % - (name, '> < = { } ( ) \' " , ; &') - ) - except (TypeError, IndexError): - raise exceptions.HTTPUnprocessableEntity( - 'Dimension name cannot be empty' - ) - - -def _validate_dimension_value(value): - try: - value[0] - if len(value) > DIMENSION_VALUE_CONSTRAINTS['MAX_LENGTH']: - raise exceptions.HTTPUnprocessableEntity( - 'Dimension value %s must be 255 characters or less' % - value - ) - except (TypeError, IndexError): - raise exceptions.HTTPUnprocessableEntity( - 'Dimension value cannot be empty' - ) - - -def validate_dimensions(dimensions): - """Validates dimensions type. - - Empty dimensions are not being validated. - For details see: - - :param dict dimensions: dimensions to validate - - * :py:data:`DIMENSION_NAME_CONSTRAINTS` - * :py:data:`DIMENSION_VALUE_CONSTRAINTS` - """ - try: - for dim_name, dim_value in dimensions.items(): - _validate_dimension_name(dim_name) - _validate_dimension_value(dim_value) - except AttributeError: - raise exceptions.HTTPUnprocessableEntity( - 'Dimensions %s must be a dictionary (map)' % dimensions) - - -def validate_content_type(req, allowed): - """Validates content type. - - Method validates request against correct - content type. - - If content-type cannot be established (i.e. header is missing), - :py:class:`falcon.HTTPMissingHeader` is thrown. - If content-type is not **application/json** or **text/plain**, - :py:class:`falcon.HTTPUnsupportedMediaType` is thrown. - - - :param falcon.Request req: current request - :param iterable allowed: allowed content type - - :exception: :py:class:`falcon.HTTPMissingHeader` - :exception: :py:class:`falcon.HTTPUnsupportedMediaType` - """ - content_type = req.content_type - - LOG.debug('Content-Type is %s', content_type) - - if content_type is None or len(content_type) == 0: - raise falcon.HTTPMissingHeader('Content-Type') - - if content_type not in allowed: - sup_types = ', '.join(allowed) - details = ('Only [%s] are accepted as logs representations' - % str(sup_types)) - raise falcon.HTTPUnsupportedMediaType(description=details) - - -def validate_payload_size(req): - """Validates payload size. - - Method validates sent payload size. - It expects that http header **Content-Length** is present. - If it does not, method raises :py:class:`falcon.HTTPLengthRequired`. - Otherwise values is being compared with :: - - [service] - max_log_size = 1048576 - - **max_log_size** refers to the maximum allowed content length. - If it is exceeded :py:class:`falcon.HTTPRequestEntityTooLarge` is - thrown. - - :param falcon.Request req: current request - - :exception: :py:class:`falcon.HTTPLengthRequired` - :exception: :py:class:`falcon.HTTPRequestEntityTooLarge` - - """ - payload_size = req.content_length - max_size = CONF.service.max_log_size - - LOG.debug('Payload (content-length) is %s', str(payload_size)) - - if payload_size is None: - raise falcon.HTTPLengthRequired( - title='Content length header is missing', - description='Content length is required to estimate if ' - 'payload can be processed' - ) - - if payload_size >= max_size: - raise falcon.HTTPPayloadTooLarge( - title='Log payload size exceeded', - description='Maximum allowed size is %d bytes' % max_size - ) - - -def validate_is_delegate(roles): - delegate_roles = CONF.roles_middleware.delegate_roles - if roles and delegate_roles: - roles = roles.split(',') if isinstance(roles, six.string_types) \ - else roles - return any(x in set(delegate_roles) for x in roles) - return False - - -def validate_cross_tenant(tenant_id, cross_tenant_id, roles): - - if not validate_is_delegate(roles): - if cross_tenant_id: - raise falcon.HTTPForbidden( - 'Permission denied', - 'Projects %s cannot POST cross tenant logs' % tenant_id - ) - - -def validate_log_message(log_object): - """Validates log property. - - Log property should have message property. - - Args: - log_object (dict): log property - """ - if 'message' not in log_object: - raise exceptions.HTTPUnprocessableEntity( - 'Log property should have message' - ) - - -def validate_authorization(http_request, authorized_rules_list): - """Validates whether is authorized according to provided policy rules list. - - If authorization fails, 401 is thrown with appropriate description. - Additionally response specifies 'WWW-Authenticate' header with 'Token' - value challenging the client to use different token (the one with - different set of roles which can access the service). - """ - challenge = 'Token' - for rule in authorized_rules_list: - try: - http_request.can(rule) - return - except Exception as ex: - LOG.debug(ex) - - raise falcon.HTTPUnauthorized('Forbidden', - 'The request does not have access to this service', - challenge) diff --git a/monasca_log_api/app/controller/__init__.py b/monasca_log_api/app/controller/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/app/controller/api/__init__.py b/monasca_log_api/app/controller/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/app/controller/api/headers.py b/monasca_log_api/app/controller/api/headers.py deleted file mode 100644 index 1a6734c8..00000000 --- a/monasca_log_api/app/controller/api/headers.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2015 FUJITSU LIMITED -# -# 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 collections - -LogApiHeader = collections.namedtuple('LogApiHeader', ['name', 'is_required']) -"""Tuple describing a header.""" - -X_TENANT_ID = LogApiHeader(name='X-Tenant-Id', is_required=False) -X_ROLES = LogApiHeader(name='X-Roles', is_required=False) -X_APPLICATION_TYPE = LogApiHeader(name='X-Application-Type', is_required=False) -X_DIMENSIONS = LogApiHeader(name='X_Dimensions', is_required=False) diff --git a/monasca_log_api/app/controller/api/healthcheck_api.py b/monasca_log_api/app/controller/api/healthcheck_api.py deleted file mode 100644 index a1eda841..00000000 --- a/monasca_log_api/app/controller/api/healthcheck_api.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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 collections - -import falcon -from oslo_log import log - -LOG = log.getLogger(__name__) - -HealthCheckResult = collections.namedtuple('HealthCheckResult', - ['status', 'details']) - - -# TODO(feature) monasca-common candidate -class HealthChecksApi(object): - """HealthChecks Api - - HealthChecksApi server information regarding health of the API. - - """ - - def __init__(self): - super(HealthChecksApi, self).__init__() - LOG.info('Initializing HealthChecksApi!') - - def on_get(self, req, res): - """Complex healthcheck report on GET. - - Returns complex report regarding API well being - and all dependent services. - - :param falcon.Request req: current request - :param falcon.Response res: current response - """ - res.status = falcon.HTTP_501 - - def on_head(self, req, res): - """Simple healthcheck report on HEAD. - - In opposite to :py:meth:`.HealthChecksApi.on_get`, this - method is supposed to execute ASAP to inform user that - API is up and running. - - :param falcon.Request req: current request - :param falcon.Response res: current response - - """ - res.status = falcon.HTTP_501 diff --git a/monasca_log_api/app/controller/api/logs_api.py b/monasca_log_api/app/controller/api/logs_api.py deleted file mode 100644 index 8d2f8b88..00000000 --- a/monasca_log_api/app/controller/api/logs_api.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2016 FUJITSU LIMITED -# -# 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 falcon -from oslo_log import log - -from monasca_log_api import conf -from monasca_log_api.monitoring import client -from monasca_log_api.monitoring import metrics - -CONF = conf.CONF -LOG = log.getLogger(__name__) - - -class LogsApi(object): - """Logs API. - - Logs API acts as RESTful endpoint accepting - messages contains collected log entries from the system. - Works as gateway for any further processing for accepted data. - - """ - def __init__(self): - super(LogsApi, self).__init__() - if CONF.monitoring.enable: - self._statsd = client.get_client() - - # create_common counters, gauges etc. - self._metrics_dimensions = dimensions = {'version': self.version} - - self._logs_in_counter = self._statsd.get_counter( - name=metrics.LOGS_RECEIVED_METRIC, - dimensions=dimensions - ) - self._logs_size_gauge = self._statsd.get_gauge( - name=metrics.LOGS_RECEIVED_BYTE_SIZE_METRICS, - dimensions=dimensions - ) - self._logs_rejected_counter = self._statsd.get_counter( - name=metrics.LOGS_REJECTED_METRIC, - dimensions=dimensions - ) - self._logs_processing_time = self._statsd.get_timer( - name=metrics.LOGS_PROCESSING_TIME_METRIC, - dimensions=dimensions - ) - - LOG.info('Initializing LogsApi %s!' % self.version) - - def on_post(self, req, res): - """Accepts sent logs as text or json. - - Accepts logs sent to resource which should - be sent to kafka queue. - - :param req: current request - :param res: current response - - """ - res.status = falcon.HTTP_501 # pragma: no cover - - def on_get(self, req, res): - """Queries logs matching specified dimension values. - - Performs queries on the underlying log storage - against a time range and set of dimension values. - - :param req: current request - :param res: current response - - """ - res.status = falcon.HTTP_501 # pragma: no cover - - @property - def version(self): - return getattr(self, 'VERSION') diff --git a/monasca_log_api/app/controller/api/versions_api.py b/monasca_log_api/app/controller/api/versions_api.py deleted file mode 100644 index bdc078f8..00000000 --- a/monasca_log_api/app/controller/api/versions_api.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2015 FUJITSU LIMITED -# -# 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 falcon -from oslo_log import log - -LOG = log.getLogger(__name__) - - -class VersionsAPI(object): - """Versions API - - VersionsAPI returns information about API itself. - - """ - - def __init__(self): - super(VersionsAPI, self).__init__() - LOG.info('Initializing VersionsAPI!') - - def on_get(self, req, res, version_id): - res.status = falcon.HTTP_501 diff --git a/monasca_log_api/app/controller/healthchecks.py b/monasca_log_api/app/controller/healthchecks.py deleted file mode 100644 index 93ba85fb..00000000 --- a/monasca_log_api/app/controller/healthchecks.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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 falcon -from monasca_log_api.common.rest import utils as rest_utils - -from monasca_log_api.app.base.validation import validate_authorization -from monasca_log_api.app.controller.api import healthcheck_api -from monasca_log_api.healthcheck import kafka_check - - -class HealthChecks(healthcheck_api.HealthChecksApi): - # response configuration - CACHE_CONTROL = ['must-revalidate', 'no-cache', 'no-store'] - - # response codes - HEALTHY_CODE_GET = falcon.HTTP_OK - HEALTHY_CODE_HEAD = falcon.HTTP_NO_CONTENT - NOT_HEALTHY_CODE = falcon.HTTP_SERVICE_UNAVAILABLE - - def __init__(self): - self._kafka_check = kafka_check.KafkaHealthCheck() - super(HealthChecks, self).__init__() - - def on_head(self, req, res): - validate_authorization(req, ['log_api:healthcheck:head']) - res.status = self.HEALTHY_CODE_HEAD - res.cache_control = self.CACHE_CONTROL - - def on_get(self, req, res): - # at this point we know API is alive, so - # keep up good work and verify kafka status - validate_authorization(req, ['log_api:healthcheck:get']) - kafka_result = self._kafka_check.healthcheck() - - # in case it'd be unhealthy, - # message will contain error string - status_data = { - 'kafka': kafka_result.message - } - - # Really simple approach, ideally that should be - # part of monasca-common with some sort of registration of - # healthchecks concept - - res.status = (self.HEALTHY_CODE_GET - if kafka_result.healthy else self.NOT_HEALTHY_CODE) - res.cache_control = self.CACHE_CONTROL - res.body = rest_utils.as_json(status_data) diff --git a/monasca_log_api/app/controller/v2/__init__.py b/monasca_log_api/app/controller/v2/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/app/controller/v2/aid/__init__.py b/monasca_log_api/app/controller/v2/aid/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/app/controller/v2/aid/service.py b/monasca_log_api/app/controller/v2/aid/service.py deleted file mode 100644 index 29fd7786..00000000 --- a/monasca_log_api/app/controller/v2/aid/service.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2016 FUJITSU LIMITED -# -# 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 datetime - -from oslo_config import cfg -from oslo_log import log - -from monasca_log_api.app.base import exceptions -from monasca_log_api.app.base import model -from monasca_log_api.app.base import validation -from monasca_log_api import conf - -LOG = log.getLogger(__name__) -CONF = conf.CONF - -EPOCH_START = datetime.datetime(1970, 1, 1) - - -class LogCreator(object): - """Transforms logs, - - Takes care of transforming information received via - HTTP requests into log and log envelopes objects. - - For more details see following: - - * :py:func:`LogCreator.new_log` - * :py:func:`LogCreator.new_log_envelope` - - """ - - def __init__(self): - self._log = log.getLogger('service.LogCreator') - self._log.info('Initializing LogCreator') - - @staticmethod - def _create_meta_info(tenant_id): - """Creates meta block for log envelope. - - Additionally method accesses oslo configuration, - looking for *service.region* configuration property. - - For more details see :py:data:`service_opts` - - :param tenant_id: ID of the tenant - :type tenant_id: str - :return: meta block - :rtype: dict - - """ - return { - 'tenantId': tenant_id, - 'region': cfg.CONF.service.region - } - - def new_log(self, - application_type, - dimensions, - payload, - content_type='application/json', - validate=True): - """Creates new log object. - - :param str application_type: origin of the log - :param dict dimensions: dictionary of dimensions (any data sent to api) - :param stream payload: stream to read log entry from - :param str content_type: actual content type used to send data to - server - :param bool validate: by default True, marks if log should be validated - :return: log object - :rtype: dict - - :keyword: log_object - """ - - if not payload: - return None - - # normalize_yet_again - application_type = parse_application_type(application_type) - dimensions = parse_dimensions(dimensions) - - if validate: - self._log.debug('Validation enabled, proceeding with validation') - validation.validate_application_type(application_type) - validation.validate_dimensions(dimensions) - - self._log.debug( - 'application_type=%s,dimensions=%s' % ( - application_type, dimensions) - ) - - log_object = {} - if content_type == 'application/json': - log_object.update(payload) - else: - log_object.update({'message': payload}) - - validation.validate_log_message(log_object) - - dimensions['component'] = application_type - log_object.update({'dimensions': dimensions}) - - return log_object - - def new_log_envelope(self, log_object, tenant_id): - return model.Envelope( - log=log_object, - meta=self._create_meta_info(tenant_id) - ) - - -def parse_application_type(app_type): - if app_type: - app_type = app_type.strip() - return app_type if app_type else None - - -def parse_dimensions(dimensions): - if not dimensions: - raise exceptions.HTTPUnprocessableEntity('Dimension are required') - - new_dimensions = {} - dimensions = map(str.strip, dimensions.split(',')) - - for dim in dimensions: - if not dim: - raise exceptions.HTTPUnprocessableEntity( - 'Dimension cannot be empty') - elif ':' not in dim: - raise exceptions.HTTPUnprocessableEntity( - '%s is not a valid dimension' % dim) - - dim = dim.split(':') - name = str(dim[0].strip()) if dim[0] else None - value = str(dim[1].strip()) if dim[1] else None - if name and value: - new_dimensions.update({name: value}) - - return new_dimensions diff --git a/monasca_log_api/app/controller/v2/logs.py b/monasca_log_api/app/controller/v2/logs.py deleted file mode 100644 index 5dc59186..00000000 --- a/monasca_log_api/app/controller/v2/logs.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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 falcon -import six - - -from monasca_log_api.app.base import log_publisher -from monasca_log_api.app.base.validation import validate_authorization -from monasca_log_api.app.controller.api import headers -from monasca_log_api.app.controller.api import logs_api -from monasca_log_api.app.controller.v2.aid import service -from monasca_log_api import conf - -CONF = conf.CONF -_DEPRECATED_INFO = ('/v2.0/log/single has been deprecated. ' - 'Please use /v3.0/logs') - - -class Logs(logs_api.LogsApi): - """Logs Api V2.""" - - VERSION = 'v2.0' - SUPPORTED_CONTENT_TYPES = {'application/json', 'text/plain'} - - def __init__(self): - self._log_creator = service.LogCreator() - self._kafka_publisher = log_publisher.LogPublisher() - super(Logs, self).__init__() - - @falcon.deprecated(_DEPRECATED_INFO) - def on_post(self, req, res): - validate_authorization(req, ['log_api:logs:post']) - if CONF.monitoring.enable: - with self._logs_processing_time.time(name=None): - self.process_on_post_request(req, res) - else: - self.process_on_post_request(req, res) - - def process_on_post_request(self, req, res): - try: - req.validate(self.SUPPORTED_CONTENT_TYPES) - tenant_id = (req.project_id if req.project_id - else req.cross_project_id) - - log = self.get_log(request=req) - envelope = self.get_envelope( - log=log, - tenant_id=tenant_id - ) - if CONF.monitoring.enable: - self._logs_size_gauge.send(name=None, - value=int(req.content_length)) - self._logs_in_counter.increment() - except Exception: - # any validation that failed means - # log is invalid and rejected - if CONF.monitoring.enable: - self._logs_rejected_counter.increment() - raise - - self._kafka_publisher.send_message(envelope) - - res.status = falcon.HTTP_204 - res.add_link( - target=str(_get_v3_link(req)), - rel='current', # [RFC5005] - title='V3 Logs', - type_hint='application/json' - ) - res.append_header('DEPRECATED', 'true') - - def get_envelope(self, log, tenant_id): - return self._log_creator.new_log_envelope( - log_object=log, - tenant_id=tenant_id - ) - - def get_log(self, request): - return self._log_creator.new_log( - application_type=request.get_header(*headers.X_APPLICATION_TYPE), - dimensions=request.get_header(*headers.X_DIMENSIONS), - payload=request.media, - content_type=request.content_type - ) - - -def _get_v3_link(req): - self_uri = req.uri - if six.PY2: - self_uri = self_uri.decode('UTF-8') - base_uri = self_uri.replace(req.relative_uri, '') - return '%s/v3.0/logs' % base_uri diff --git a/monasca_log_api/app/controller/v3/__init__.py b/monasca_log_api/app/controller/v3/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/app/controller/v3/aid/__init__.py b/monasca_log_api/app/controller/v3/aid/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/app/controller/v3/aid/bulk_processor.py b/monasca_log_api/app/controller/v3/aid/bulk_processor.py deleted file mode 100644 index 8a431930..00000000 --- a/monasca_log_api/app/controller/v3/aid/bulk_processor.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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 oslo_log import log - -from monasca_log_api.app.base import log_publisher -from monasca_log_api.app.base import model -from monasca_log_api.app.base import validation -from monasca_log_api import conf - -LOG = log.getLogger(__name__) -CONF = conf.CONF - - -class BulkProcessor(log_publisher.LogPublisher): - """BulkProcessor for effective log processing and publishing. - - BulkProcessor is customized version of - :py:class:`monasca_log_api.app.base.log_publisher.LogPublisher` - that utilizes processing of bulk request inside single loop. - - """ - - def __init__(self, logs_in_counter=None, logs_rejected_counter=None): - """Initializes BulkProcessor. - - :param logs_in_counter: V3 received logs counter - :param logs_rejected_counter: V3 rejected logs counter - """ - super(BulkProcessor, self).__init__() - - if CONF.monitoring.enable: - assert logs_in_counter is not None - assert logs_rejected_counter is not None - self._logs_in_counter = logs_in_counter - self._logs_rejected_counter = logs_rejected_counter - - self.service_region = CONF.service.region - - def send_message(self, logs, global_dimensions=None, log_tenant_id=None): - """Sends bulk package to kafka - - :param list logs: received logs - :param dict global_dimensions: global dimensions for each log - :param str log_tenant_id: tenant who sent logs - """ - - num_of_msgs = len(logs) if logs else 0 - sent_count = 0 - to_send_msgs = [] - - LOG.debug('Bulk package ', - num_of_msgs, global_dimensions, log_tenant_id) - - try: - for log_el in logs: - t_el = self._transform_message(log_el, - global_dimensions, - log_tenant_id) - if t_el: - to_send_msgs.append(t_el) - if CONF.monitoring.enable: - with self._publish_time_ms.time(name=None): - self._publish(to_send_msgs) - else: - self._publish(to_send_msgs) - - sent_count = len(to_send_msgs) - - except Exception as ex: - LOG.error('Failed to send bulk package ', - num_of_msgs, global_dimensions) - LOG.exception(ex) - raise ex - finally: - if CONF.monitoring.enable: - self._update_counters(len(to_send_msgs), num_of_msgs) - self._after_publish(sent_count, len(to_send_msgs)) - - def _update_counters(self, in_counter, to_send_counter): - rejected_counter = to_send_counter - in_counter - - self._logs_in_counter.increment(value=in_counter) - self._logs_rejected_counter.increment(value=rejected_counter) - - def _transform_message(self, log_element, *args): - try: - validation.validate_log_message(log_element) - - log_envelope = model.Envelope.new_envelope( - log=log_element, - tenant_id=args[1], - region=self.service_region, - dimensions=self._get_dimensions(log_element, - global_dims=args[0]) - ) - - msg_payload = (super(BulkProcessor, self) - ._transform_message(log_envelope)) - - return msg_payload - except Exception as ex: - LOG.error('Log transformation failed, rejecting log') - LOG.exception(ex) - - return None - - def _create_envelope(self, log_element, tenant_id, dimensions=None): - """Create a log envelope. - - :param dict log_element: raw log element - :param str tenant_id: tenant who sent logs - :param dict dimensions: log dimensions - :return: log envelope - :rtype: model.Envelope - - """ - return - - def _get_dimensions(self, log_element, global_dims=None): - """Get the dimensions of log element. - - If global dimensions are specified and passed to this method, - both instances are merged with each other. - - If neither is specified empty dictionary is returned. - - If only local dimensions are specified they are returned without any - additional operations. The last statement applies also - to global dimensions. - - :param dict log_element: raw log instance - :param dict global_dims: global dimensions or None - :return: local dimensions merged with global dimensions - :rtype: dict - """ - local_dims = log_element.get('dimensions', {}) - - if not global_dims: - global_dims = {} - if local_dims: - validation.validate_dimensions(local_dims) - - dimensions = global_dims.copy() - dimensions.update(local_dims) - - return dimensions diff --git a/monasca_log_api/app/controller/v3/aid/helpers.py b/monasca_log_api/app/controller/v3/aid/helpers.py deleted file mode 100644 index c5ad1872..00000000 --- a/monasca_log_api/app/controller/v3/aid/helpers.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2014 Hewlett-Packard -# Copyright 2015 Cray Inc. All Rights Reserved. -# Copyright 2016 Hewlett Packard Enterprise Development Company LP -# Copyright 2016 FUJITSU LIMITED -# -# 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 falcon -from monasca_log_api.common.rest import utils as rest_utils -from oslo_log import log - -from monasca_log_api.app.base import exceptions -from monasca_log_api.app.base import validation - -LOG = log.getLogger(__name__) - - -def read_json_msg_body(req): - """Read the json_msg from the http request body and return them as JSON. - - :param req: HTTP request object. - :return: Returns the metrics as a JSON object. - :raises falcon.HTTPBadRequest: - """ - try: - body = req.media - - if body is not None: - return body - else: - raise falcon.HTTPBadRequest('Bad request', - 'Request body is Empty') - - except rest_utils.exceptions.DataConversionException as ex: - LOG.debug(ex) - raise falcon.HTTPBadRequest('Bad request', - 'Request body is not valid JSON') - except ValueError as ex: - LOG.debug(ex) - raise falcon.HTTPBadRequest('Bad request', - 'Request body is not valid JSON') - - -def get_global_dimensions(request_body): - """Get the top level dimensions in the HTTP request body.""" - global_dims = request_body.get('dimensions', {}) - validation.validate_dimensions(global_dims) - return global_dims - - -def get_logs(request_body): - """Get the logs in the HTTP request body.""" - if 'logs' not in request_body: - raise exceptions.HTTPUnprocessableEntity( - 'Unprocessable Entity Logs not found') - return request_body['logs'] diff --git a/monasca_log_api/app/controller/v3/logs.py b/monasca_log_api/app/controller/v3/logs.py deleted file mode 100644 index 349382b9..00000000 --- a/monasca_log_api/app/controller/v3/logs.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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 falcon -from oslo_log import log - -from monasca_log_api.app.base import exceptions -from monasca_log_api.app.base import validation -from monasca_log_api.app.controller.api import logs_api -from monasca_log_api.app.controller.v3.aid import bulk_processor -from monasca_log_api.app.controller.v3.aid import helpers -from monasca_log_api import conf -from monasca_log_api.monitoring import metrics - - -CONF = conf.CONF -LOG = log.getLogger(__name__) -_LOG_API_DEPRECATED = ('This API has been deprecated. Please use ' - 'monasca-api/logs') - - -class Logs(logs_api.LogsApi): - - VERSION = 'v3.0' - SUPPORTED_CONTENT_TYPES = {'application/json'} - - def __init__(self): - super(Logs, self).__init__() - - if CONF.monitoring.enable: - self._processor = bulk_processor.BulkProcessor( - logs_in_counter=self._logs_in_counter, - logs_rejected_counter=self._logs_rejected_counter - ) - self._bulks_rejected_counter = self._statsd.get_counter( - name=metrics.LOGS_BULKS_REJECTED_METRIC, - dimensions=self._metrics_dimensions - ) - else: - self._processor = bulk_processor.BulkProcessor() - - @falcon.deprecated(_LOG_API_DEPRECATED) - def on_post(self, req, res): - validation.validate_authorization(req, ['log_api:logs:post']) - if CONF.monitoring.enable: - with self._logs_processing_time.time(name=None): - self.process_on_post_request(req, res) - else: - self.process_on_post_request(req, res) - - def process_on_post_request(self, req, res): - try: - req.validate(self.SUPPORTED_CONTENT_TYPES) - - request_body = helpers.read_json_msg_body(req) - - log_list = self._get_logs(request_body) - global_dimensions = self._get_global_dimensions(request_body) - - except Exception as ex: - LOG.error('Entire bulk package has been rejected') - LOG.exception(ex) - if CONF.monitoring.enable: - self._bulks_rejected_counter.increment(value=1) - - raise ex - - if CONF.monitoring.enable: - self._bulks_rejected_counter.increment(value=0) - self._logs_size_gauge.send(name=None, - value=int(req.content_length)) - - tenant_id = (req.cross_project_id if req.cross_project_id - else req.project_id) - - try: - self._processor.send_message( - logs=log_list, - global_dimensions=global_dimensions, - log_tenant_id=tenant_id - ) - except Exception as ex: - res.status = getattr(ex, 'status', falcon.HTTP_500) - return - - res.status = falcon.HTTP_204 - - @staticmethod - def _get_global_dimensions(request_body): - """Get the top level dimensions in the HTTP request body.""" - global_dims = request_body.get('dimensions', {}) - validation.validate_dimensions(global_dims) - return global_dims - - @staticmethod - def _get_logs(request_body): - """Get the logs in the HTTP request body.""" - if 'logs' not in request_body: - raise exceptions.HTTPUnprocessableEntity( - 'Unprocessable Entity Logs not found') - return request_body['logs'] diff --git a/monasca_log_api/app/controller/versions.py b/monasca_log_api/app/controller/versions.py deleted file mode 100644 index 972d15e0..00000000 --- a/monasca_log_api/app/controller/versions.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2016 FUJITSU LIMITED -# -# 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 falcon -import six - -from monasca_log_api.common.rest import utils as rest_utils - -from monasca_log_api.app.base.validation import validate_authorization -from monasca_log_api.app.controller.api import versions_api - -_VERSIONS_TPL_DICT = { - 'v2.0': { - 'id': 'v2.0', - 'links': [ - { - 'rel': 'logs', - 'href': '/log/single' - } - ], - 'status': 'DEPRECATED', - 'updated': "2015-09-01T00:00:00Z" - }, - 'v3.0': { - 'id': 'v3.0', - 'links': [ - { - 'rel': 'logs', - 'href': '/logs' - } - ], - 'status': 'CURRENT', - 'updated': "2016-03-01T00:00:00Z" - } -} - - -class Versions(versions_api.VersionsAPI): - """Versions Api""" - - @staticmethod - def handle_none_version_id(req, res, result): - for version in _VERSIONS_TPL_DICT: - selected_version = _parse_version(version, req) - result['elements'].append(selected_version) - res.body = rest_utils.as_json(result, sort_keys=True) - res.status = falcon.HTTP_200 - - @staticmethod - def handle_version_id(req, res, result, version_id): - if version_id in _VERSIONS_TPL_DICT: - result['elements'].append(_parse_version(version_id, req)) - res.body = rest_utils.as_json(result, sort_keys=True) - res.status = falcon.HTTP_200 - else: - error_body = {'message': '%s is not valid version' % version_id} - res.body = rest_utils.as_json(error_body) - res.status = falcon.HTTP_400 - - def on_get(self, req, res, version_id=None): - validate_authorization(req, ['log_api:versions:get']) - result = { - 'links': _get_common_links(req), - 'elements': [] - } - if version_id is None: - self.handle_none_version_id(req, res, result) - else: - self.handle_version_id(req, res, result, version_id) - - -def _get_common_links(req): - self_uri = req.uri - if six.PY2: - self_uri = self_uri.decode(rest_utils.ENCODING) - base_uri = self_uri.rpartition(req.path)[0] - return [ - { - 'rel': 'self', - 'href': self_uri - }, - { - 'rel': 'version', - 'href': '%s/version' % base_uri - }, - { - 'rel': 'healthcheck', - 'href': '%s/healthcheck' % base_uri - } - ] - - -def _parse_version(version_id, req): - self_uri = req.uri - if six.PY2: - self_uri = self_uri.decode(rest_utils.ENCODING) - base_uri = self_uri.rpartition(req.path)[0] - - # need to get template dict, consecutive calls - # needs to operate on unmodified instance - - selected_version = _VERSIONS_TPL_DICT[version_id].copy() - raw_links = selected_version['links'] - links = [] - - for link in raw_links: - raw_link_href = link.get('href') - raw_link_rel = link.get('rel') - link_href = base_uri + '/' + version_id + raw_link_href - links.append({ - 'href': link_href, - 'rel': raw_link_rel - }) - selected_version['links'] = links - - return selected_version diff --git a/monasca_log_api/app/main.py b/monasca_log_api/app/main.py deleted file mode 100644 index 16e7a8c7..00000000 --- a/monasca_log_api/app/main.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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. - -""" -Allows to run monasca-log-api from within local [dev] environment. -Primarily used for development. -""" - -import sys - -from paste import deploy -from paste import httpserver - -from monasca_log_api import version - - -def get_wsgi_app(): - config_dir = 'etc/monasca' - - return deploy.loadapp( - 'config:%s/log-api-paste.ini' % config_dir, - relative_to='./', - name='main' - ) - - -def main(): - wsgi_app = get_wsgi_app() - server_version = 'log-api/%s' % version.version_str - - server = httpserver.serve(application=wsgi_app, host='127.0.0.1', - port=5607, server_version=server_version) - return server - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/monasca_log_api/app/wsgi.py b/monasca_log_api/app/wsgi.py deleted file mode 100644 index 61cd25fc..00000000 --- a/monasca_log_api/app/wsgi.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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. - -""" -Use this file for deploying the API under mod_wsgi. -""" - -from paste import deploy - -application = None - - -def main(): - base_dir = '/etc/monasca/' - conf = '%slog-api-paste.ini' % base_dir - app = deploy.loadapp('config:%s' % conf) - return app - - -if __name__ == '__main__' or __name__.startswith('_mod_wsgi'): - application = main() diff --git a/monasca_log_api/common/rest/__init__.py b/monasca_log_api/common/rest/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/common/rest/exceptions.py b/monasca_log_api/common/rest/exceptions.py deleted file mode 100644 index 7054ff97..00000000 --- a/monasca_log_api/common/rest/exceptions.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2015 FUJITSU LIMITED -# -# 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. - - -class UnsupportedContentTypeException(Exception): - """Exception thrown if content type is not supported.""" - pass - - -class UnreadableContentError(IOError): - """Exception thrown if reading data fails - - :py:class`.UnreadableContentError` may be thrown - if data was impossible to read from input - - """ - pass - - -class DataConversionException(Exception): - """Exception thrown if data transformation fails - - :py:class`.DataConversionException` may be thrown - if data was impossible to transform into target - representation according to content_type classifier. - - """ - pass diff --git a/monasca_log_api/common/rest/utils.py b/monasca_log_api/common/rest/utils.py deleted file mode 100644 index abda2b65..00000000 --- a/monasca_log_api/common/rest/utils.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2015 FUJITSU LIMITED -# -# 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 simplejson as json -import six - -from monasca_log_api.common.rest import exceptions - -ENCODING = 'utf8' - -TEXT_CONTENT_TYPE = 'text/plain' -JSON_CONTENT_TYPE = 'application/json' - - -def _try_catch(fun): - - @six.wraps(fun) - def wrapper(*args, **kwargs): - try: - return fun(*args, **kwargs) - except Exception as ex: - raise exceptions.DataConversionException(str(ex)) - - return wrapper - - -@_try_catch -def as_json(data, **kwargs): - """Writes data as json. - - :param dict data: data to convert to json - :param kwargs kwargs: kwargs for json dumps - :return: json string - :rtype: str - """ - - if 'sort_keys' not in kwargs: - kwargs['sort_keys'] = False - if 'ensure_ascii' not in kwargs: - kwargs['ensure_ascii'] = False - - data = json.dumps(data, **kwargs) - - return data - - -@_try_catch -def from_json(data, **kwargs): - """Reads data from json str. - - :param str data: data to read - :param kwargs kwargs: kwargs for json loads - :return: read data - :rtype: dict - """ - return json.loads(data, **kwargs) - - -_READABLE_CONTENT_TYPES = { - TEXT_CONTENT_TYPE: lambda content: content, - JSON_CONTENT_TYPE: from_json -} - - -def read_body(payload, content_type=JSON_CONTENT_TYPE): - """Reads HTTP payload according to given content_type. - - Function is capable of reading from payload stream. - Read data is then processed according to content_type. - - Note: - Content-Type is validated. It means that if read_body - body is not capable of reading data in requested type, - it will throw an exception. - - If read data was empty method will return false boolean - value to indicate that. - - Note: - There is no transformation if content type is equal to - 'text/plain'. What has been read is returned. - - :param stream payload: payload to read, payload should have read method - :param str content_type: payload content type, default to application/json - :return: read data, returned type depends on content_type or False - if empty - - :exception: :py:class:`.UnreadableBody` - in case of any failure when - reading data - - """ - if content_type not in _READABLE_CONTENT_TYPES: - msg = ('Cannot read %s, not in %s' % - (content_type, _READABLE_CONTENT_TYPES)) - raise exceptions.UnsupportedContentTypeException(msg) - - try: - content = payload.read() - if not content: - return None - except Exception as ex: - raise exceptions.UnreadableContentError(str(ex)) - - return _READABLE_CONTENT_TYPES[content_type](content) diff --git a/monasca_log_api/conf/__init__.py b/monasca_log_api/conf/__init__.py deleted file mode 100644 index 7d4dfe8a..00000000 --- a/monasca_log_api/conf/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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 os -import pkgutil - -from oslo_config import cfg -from oslo_log import log -from oslo_utils import importutils - -CONF = cfg.CONF -LOG = log.getLogger(__name__) - - -def load_conf_modules(): - """Loads all modules that contain configuration - - Method iterates over modules of :py:module:`monasca_log_api.conf` - and imports only those that contain following methods: - - - list_opts (required by oslo_config.genconfig) - - register_opts (required by :py:currentmodule:) - - """ - for modname in _list_module_names(): - mod = importutils.import_module('monasca_log_api.conf.' + modname) - required_funcs = ['register_opts', 'list_opts'] - for func in required_funcs: - if hasattr(mod, func): - yield mod - - -def _list_module_names(): - package_path = os.path.dirname(os.path.abspath(__file__)) - for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]): - if not (modname == "opts" and ispkg): - yield modname - - -def register_opts(): - """Registers all conf modules opts - - This method allows different modules to register - opts according to their needs. - - """ - for mod in load_conf_modules(): - mod.register_opts(CONF) - - -def list_opts(): - """Lists all conf modules opts. - - Goes through all conf modules and yields their opts - - """ - for mod in load_conf_modules(): - mod_opts = mod.list_opts() - yield mod_opts[0], mod_opts[1] diff --git a/monasca_log_api/conf/healthcheck.py b/monasca_log_api/conf/healthcheck.py deleted file mode 100644 index 00cf215b..00000000 --- a/monasca_log_api/conf/healthcheck.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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 oslo_config import cfg - -kafka_check_opts = [ - cfg.StrOpt('kafka_url', - required=True, - help='Url to kafka server'), - cfg.ListOpt('kafka_topics', - required=True, - default=['logs'], - help='Verify existence of configured topics') -] -kafka_check_group = cfg.OptGroup(name='kafka_healthcheck', - title='kafka_healthcheck') - - -def register_opts(conf): - conf.register_group(kafka_check_group) - conf.register_opts(kafka_check_opts, kafka_check_group) - - -def list_opts(): - return kafka_check_group, kafka_check_opts diff --git a/monasca_log_api/conf/log_publisher.py b/monasca_log_api/conf/log_publisher.py deleted file mode 100644 index 5a770729..00000000 --- a/monasca_log_api/conf/log_publisher.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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 oslo_config import cfg - -_MAX_MESSAGE_SIZE = 1048576 - -log_publisher_opts = [ - cfg.StrOpt('kafka_url', - required=True, - help='Url to kafka server'), - cfg.MultiStrOpt('topics', - default=['logs'], - help='Consumer topics'), - cfg.IntOpt('max_message_size', - default=_MAX_MESSAGE_SIZE, - required=True, - help=('Message max size that can be sent ' - 'to kafka, default to %d bytes' % _MAX_MESSAGE_SIZE)) -] -log_publisher_group = cfg.OptGroup(name='log_publisher', title='log_publisher') - - -def register_opts(conf): - conf.register_group(log_publisher_group) - conf.register_opts(log_publisher_opts, log_publisher_group) - - -def list_opts(): - return log_publisher_group, log_publisher_opts diff --git a/monasca_log_api/conf/monitoring.py b/monasca_log_api/conf/monitoring.py deleted file mode 100644 index 5567a687..00000000 --- a/monasca_log_api/conf/monitoring.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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 oslo_config import cfg - -_DEFAULT_HOST = '127.0.0.1' -_DEFAULT_PORT = 8125 -_DEFAULT_BUFFER_SIZE = 50 - -monitoring_opts = [ - cfg.BoolOpt('enable', - default=True, - help='Determine if self monitoring is enabled'), - cfg.HostAddressOpt('statsd_host', - default=_DEFAULT_HOST, - help=('IP address or host domain name of statsd server, default to %s' - % _DEFAULT_HOST)), - cfg.PortOpt('statsd_port', - default=_DEFAULT_PORT, - help='Port of statsd server, default to %d' % _DEFAULT_PORT), - cfg.IntOpt('statsd_buffer', - default=_DEFAULT_BUFFER_SIZE, - required=True, - help=('Maximum number of metric to buffer before sending, ' - 'default to %d' % _DEFAULT_BUFFER_SIZE)), - cfg.DictOpt('dimensions', default={}, - required=False, help='Additional dimensions that can be set') -] -monitoring_group = cfg.OptGroup(name='monitoring', title='monitoring') - - -def register_opts(conf): - conf.register_group(monitoring_group) - conf.register_opts(monitoring_opts, monitoring_group) - - -def list_opts(): - return monitoring_group, monitoring_opts diff --git a/monasca_log_api/conf/role_middleware.py b/monasca_log_api/conf/role_middleware.py deleted file mode 100644 index bb460f47..00000000 --- a/monasca_log_api/conf/role_middleware.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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 oslo_config import cfg - -role_m_opts = [ - cfg.ListOpt(name='path', - default='/', - help='List of paths where middleware applies to'), - cfg.ListOpt(name='default_roles', - default=['monasca-user'], - help='List of roles allowed to enter api'), - cfg.ListOpt(name='agent_roles', - default=None, - help=('List of roles, that if set, mean that request ' - 'comes from agent, thus is authorized in the same ' - 'time')), - cfg.ListOpt(name='delegate_roles', - default=['admin'], - help=('Roles that are allowed to POST logs on ' - 'behalf of another tenant (project)')), - cfg.ListOpt(name='check_roles', - default=['@'], - help=('Roles that are allowed to do check ' - 'version and health')) -] -role_m_group = cfg.OptGroup(name='roles_middleware', title='roles_middleware') - - -def register_opts(conf): - conf.register_group(role_m_group) - conf.register_opts(role_m_opts, role_m_group) - - -def list_opts(): - return role_m_group, role_m_opts diff --git a/monasca_log_api/conf/service.py b/monasca_log_api/conf/service.py deleted file mode 100644 index 0ce2496d..00000000 --- a/monasca_log_api/conf/service.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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 oslo_config import cfg - -_DEFAULT_MAX_LOG_SIZE = 1024 * 1024 - -service_opts = [ - cfg.StrOpt('region', - default=None, - help='Region'), - cfg.IntOpt('max_log_size', - default=_DEFAULT_MAX_LOG_SIZE, - help=('Refers to payload/envelope size. If either is exceeded' - 'API will throw an error')) -] -service_group = cfg.OptGroup(name='service', title='service') - - -def register_opts(conf): - conf.register_group(service_group) - conf.register_opts(service_opts, service_group) - - -def list_opts(): - return service_group, service_opts diff --git a/monasca_log_api/config.py b/monasca_log_api/config.py deleted file mode 100644 index 441a6bae..00000000 --- a/monasca_log_api/config.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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 sys - -from oslo_config import cfg -from oslo_log import log -from oslo_policy import opts as policy_opts - -from monasca_log_api import conf -from monasca_log_api import version - -CONF = conf.CONF -LOG = log.getLogger(__name__) - -_CONF_LOADED = False -_GUNICORN_MARKER = 'gunicorn' - - -def _is_running_under_gunicorn(): - """Evaluates if api runs under gunicorn""" - content = filter(lambda x: x != sys.executable and _GUNICORN_MARKER in x, - sys.argv or []) - return len(list(content) if not isinstance(content, list) else content) > 0 - - -def get_config_files(): - """Get the possible configuration files accepted by oslo.config - - This also includes the deprecated ones - """ - # default files - conf_files = cfg.find_config_files(project='monasca', - prog='monasca-log-api') - # deprecated config files (only used if standard config files are not there) - if len(conf_files) == 0: - old_conf_files = cfg.find_config_files(project='monasca', - prog='log-api') - if len(old_conf_files) > 0: - LOG.warning('Found deprecated old location "{}" ' - 'of main configuration file'.format(old_conf_files)) - conf_files += old_conf_files - return conf_files - - -def parse_args(argv=None): - global _CONF_LOADED - if _CONF_LOADED: - LOG.debug('Configuration has been already loaded') - return - - log.set_defaults() - log.register_options(CONF) - - argv = (argv if argv is not None else sys.argv[1:]) - args = ([] if _is_running_under_gunicorn() else argv or []) - - CONF(args=args, - prog=sys.argv[1:], - project='monasca', - version=version.version_str, - default_config_files=get_config_files(), - description='RESTful API to collect log files') - - log.setup(CONF, - product_name='monasca-log-api', - version=version.version_str) - - conf.register_opts() - policy_opts.set_defaults(CONF) - - _CONF_LOADED = True diff --git a/monasca_log_api/db/__init__.py b/monasca_log_api/db/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/db/common/__init__.py b/monasca_log_api/db/common/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/db/common/model.py b/monasca_log_api/db/common/model.py deleted file mode 100644 index 60c0cabd..00000000 --- a/monasca_log_api/db/common/model.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2017 StackHPC -# -# 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 collections - - -class Dimension(collections.namedtuple('Dimension', 'name values')): - """Representation of dimension names and optional values list. - - Named-tuple type to represent the pairing of a dimension name and an - optional list of values. - - :ivar name: Name of the dimension to reference. - :ivar values: Optional list of values associated with the dimension. - - :vartype name: str - :vartype values: None or list[str] - """ - - -class SortBy(collections.namedtuple('SortBy', 'field direction')): - """Representation of an individual sorting directive. - - Named-tuple type to represent a directive for indicating how a result set - should be sorted. - - :ivar field: Name of the field which is provides the values to sort by. - :ivar direction: Either 'asc' or 'desc' specifying the order of values. - - :vartype name: str - :vartype values: str - """ diff --git a/monasca_log_api/db/repo/__init__.py b/monasca_log_api/db/repo/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/db/repo/logs_repository.py b/monasca_log_api/db/repo/logs_repository.py deleted file mode 100644 index 64e9936f..00000000 --- a/monasca_log_api/db/repo/logs_repository.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2017 StackHPC -# -# 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 abc -import six - - -@six.add_metaclass(abc.ABCMeta) -class LogsRepository(object): - - def __init__(self): - super(LogsRepository, self).__init__() - - @abc.abstractmethod - def list_logs(self, tenant_id, dimensions, start_time, end_time, offset, - limit, sort_by): - """Obtain log listing based on simple criteria of dimension values. - - Performs queries on the underlying log storage against a time range and - set of dimension values. Additionally, it is possible to optionally - sort results by timestamp. - - :param tenant_id: - Tenant/project id for which to obtain logs (required). - :param dimensions: - List of Dimension tuples containing pairs of dimension names and - optional lists of dimension values. These will be used to filter - the logs returned. If no dimensions are specified, then no - filtering is performed. When multiple values are given, the - dimension must match any of the given values. If None is given, - logs with any value for the dimension will be returned. - :param start_time: - Optional starting time in UNIX time (seconds, inclusive). - :param end_time: - Optional ending time in UNIX time (seconds, inclusive). - :param offset: - Number of matching results to skip past, if specified. - :param limit: - Number of matching results to return (required). - :param sort_by: - List of SortBy tuples specifying fields to sort by and the - direction to sort the result set by. e.g. ('timestamp','asc'). The - direction is specified by either the string 'asc' for ascending - direction, or 'desc' for descending. If not specified, no order - must be enforced and the implementation is free to choose the most - efficient method to return the results. - - :type tenant_id: str - :type dimensions: None or list[Dimension[str, list[str] or None]] - :type start_time: None or int - :type end_time: None or int - :type offset: None or int - :type limit: int - :type sort_by: None or list[SortBy[str, str]] - - :return: - Log messages matching the given criteria. The dict representing - each message entry will contain attributes extracted from the - underlying structure; 'message', 'timestamp' and 'dimensions'. - - :rtype: list[dict] - """ - pass diff --git a/monasca_log_api/healthcheck/__init__.py b/monasca_log_api/healthcheck/__init__.py deleted file mode 100644 index 72fb7750..00000000 --- a/monasca_log_api/healthcheck/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Base package for monasca-log-api healthcheck""" diff --git a/monasca_log_api/healthcheck/kafka_check.py b/monasca_log_api/healthcheck/kafka_check.py deleted file mode 100644 index 71a23fc7..00000000 --- a/monasca_log_api/healthcheck/kafka_check.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2015-2017 FUJITSU LIMITED -# -# 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 collections - -from monasca_common.kafka_lib import client -from oslo_log import log -from six import PY3 - -from monasca_log_api import conf - -LOG = log.getLogger(__name__) -CONF = conf.CONF - - -CheckResult = collections.namedtuple('CheckResult', ['healthy', 'message']) -"""Result from the healthcheck, contains healthy(boolean) and message""" - - -# TODO(feature) monasca-common candidate -class KafkaHealthCheck(object): - """Evaluates kafka health - - Healthcheck verifies if: - - * kafka server is up and running - * there is a configured topic in kafka - - If following conditions are met healthcheck returns healthy status. - Otherwise unhealthy status is returned with explanation. - - Example of middleware configuration: - - .. code-block:: ini - - [kafka_healthcheck] - kafka_url = localhost:8900 - kafka_topics = log - - Note: - It is possible to specify multiple topics if necessary. - Just separate them with , - - """ - - def healthcheck(self): - url = CONF.kafka_healthcheck.kafka_url - - try: - kafka_client = client.KafkaClient(hosts=url) - except client.KafkaUnavailableError as ex: - LOG.error(repr(ex)) - error_str = 'Could not connect to kafka at %s' % url - return CheckResult(healthy=False, message=error_str) - - result = self._verify_topics(kafka_client) - self._disconnect_gracefully(kafka_client) - - return result - - # noinspection PyMethodMayBeStatic - def _verify_topics(self, kafka_client): - topics = CONF.kafka_healthcheck.kafka_topics - - if PY3: - topics = tuple(topic.encode('utf-8') for topic in topics) - - for t in topics: - # kafka client loads metadata for topics as fast - # as possible (happens in __init__), therefore this - # topic_partitions is sure to be filled - for_topic = t in kafka_client.topic_partitions - if not for_topic: - error_str = 'Kafka: Topic %s not found' % t - LOG.error(error_str) - return CheckResult(healthy=False, message=error_str) - - return CheckResult(healthy=True, message='OK') - - # noinspection PyMethodMayBeStatic - def _disconnect_gracefully(self, kafka_client): - # at this point, client is connected so it must be closed - # regardless of topic existence - try: - kafka_client.close() - except Exception as ex: - # log that something went wrong and move on - LOG.error(repr(ex)) diff --git a/monasca_log_api/middleware/__init__.py b/monasca_log_api/middleware/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/middleware/role_middleware.py b/monasca_log_api/middleware/role_middleware.py deleted file mode 100644 index cac851a8..00000000 --- a/monasca_log_api/middleware/role_middleware.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright 2015-2017 FUJITSU LIMITED -# -# 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 oslo_log import log -from oslo_middleware import base as om -from webob import response - -from monasca_log_api import conf - -CONF = conf.CONF -LOG = log.getLogger(__name__) - -_X_IDENTITY_STATUS = 'X-Identity-Status' -_X_ROLES = 'X-Roles' -_X_MONASCA_LOG_AGENT = 'X-MONASCA-LOG-AGENT' -_CONFIRMED_STATUS = 'Confirmed' - - -def _ensure_lower_roles(roles): - if not roles: - return [] - return [role.strip().lower() for role in roles] - - -def _intersect(a, b): - return list(set(a) & set(b)) - - -class RoleMiddleware(om.ConfigurableMiddleware): - """Authorization middleware for X-Roles header. - - RoleMiddleware is responsible for authorizing user's - access against **X-Roles** header. Middleware - expects authentication to be completed (i.e. keystone middleware - has been already called). - - If tenant is authenticated and authorized middleware - exits silently (that is considered a success). Otherwise - middleware produces JSON response according to following schema - - .. code-block:: javascript - - { - 'title': u'Unauthorized', - 'message': explanation (str) - } - - Configuration example - - .. code-block:: cfg - - [roles_middleware] - path = /v2.0/log - default_roles = monasca-user - agent_roles = monasca-log-agent - delegate_roles = admin - - Configuration explained: - - * path (list) - path (or list of paths) middleware should be applied - * agent_roles (list) - list of roles that identifies tenant as an agent - * default_roles (list) - list of roles that should be authorized - * delegate_roles (list) - list of roles that are allowed to POST logs on - behalf of another tenant (project) - - Note: - Being an agent means that tenant is automatically authorized. - Note: - Middleware works only for configured paths and for all - requests apart from HTTP method **OPTIONS**. - - """ - - def __init__(self, application, conf=None): - super(RoleMiddleware, self).__init__(application, conf) - middleware = CONF.roles_middleware - - self._path = middleware.path - self._default_roles = _ensure_lower_roles(middleware.default_roles) - self._agent_roles = _ensure_lower_roles(middleware.agent_roles) - - LOG.debug('RolesMiddleware initialized for paths=%s', self._path) - - def process_request(self, req): - if not self._can_apply_middleware(req): - LOG.debug('%s skipped in role middleware', req.path) - return None - - is_authenticated = self._is_authenticated(req) - is_agent = self._is_agent(req) - tenant_id = req.headers.get('X-Tenant-Id') - - req.environ[_X_MONASCA_LOG_AGENT] = is_agent - - LOG.debug('%s is authenticated=%s, log_agent=%s', - tenant_id, is_authenticated, is_agent) - - if is_authenticated: - LOG.debug('%s has been authenticated', tenant_id) - return # do return nothing to enter API internal - - explanation = u'Failed to authenticate request for %s' % tenant_id - LOG.error(explanation) - json_body = {u'title': u'Unauthorized', u'message': explanation} - return response.Response(status=401, - json_body=json_body, - content_type='application/json') - - def _is_agent(self, req): - headers = req.headers - roles = headers.get(_X_ROLES) - - if not roles: - LOG.warning('Couldn\'t locate %s header,or it was empty', _X_ROLES) - return False - else: - roles = _ensure_lower_roles(roles.split(',')) - - is_agent = len(_intersect(roles, self._agent_roles)) > 0 - - return is_agent - - def _is_authenticated(self, req): - headers = req.headers - if _X_IDENTITY_STATUS in headers: - status = req.headers.get(_X_IDENTITY_STATUS) - return _CONFIRMED_STATUS == status - return False - - def _can_apply_middleware(self, req): - path = req.path - method = req.method - - if method == 'OPTIONS': - return False - - if self._path: - for p in self._path: - if path.startswith(p): - return True - return False # if no configured paths, or nothing matches diff --git a/monasca_log_api/monitoring/__init__.py b/monasca_log_api/monitoring/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/monitoring/client.py b/monasca_log_api/monitoring/client.py deleted file mode 100644 index 0a9174b1..00000000 --- a/monasca_log_api/monitoring/client.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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 monascastatsd - -from oslo_log import log - -from monasca_log_api import conf - -LOG = log.getLogger(__name__) -CONF = conf.CONF - -_CLIENT_NAME = 'monasca' -_DEFAULT_DIMENSIONS = { - 'service': 'monitoring', - 'component': 'monasca-log-api' -} - - -def get_client(dimensions=None): - """Creates statsd client - - Creates monasca-statsd client using configuration from - config file and supplied dimensions. - - Configuration is composed out of :: - - [monitoring] - statsd_host = 192.168.10.4 - statsd_port = 8125 - statsd_buffer = 50 - - Dimensions are appended to following dictionary :: - - { - 'service': 'monitoring', - 'component': 'monasca-log-api' - } - - Note: - Passed dimensions do not override those specified in - dictionary above - - :param dict dimensions: Optional dimensions - :return: statsd client - :rtype: monascastatsd.Client - """ - dims = _DEFAULT_DIMENSIONS.copy() - if dimensions: - for key, val in dimensions.items(): - if key not in _DEFAULT_DIMENSIONS: - dims[key] = val - else: - LOG.warning('Cannot override fixed dimension %s=%s', key, - _DEFAULT_DIMENSIONS[key]) - - connection = monascastatsd.Connection( - host=CONF.monitoring.statsd_host, - port=CONF.monitoring.statsd_port, - max_buffer_size=CONF.monitoring.statsd_buffer - ) - client = monascastatsd.Client(name=_CLIENT_NAME, - connection=connection, - dimensions=dims) - - LOG.debug('Created statsd client %s[%s] = %s:%d', _CLIENT_NAME, dims, - CONF.monitoring.statsd_host, CONF.monitoring.statsd_port) - - return client diff --git a/monasca_log_api/monitoring/metrics.py b/monasca_log_api/monitoring/metrics.py deleted file mode 100644 index 97e3677b..00000000 --- a/monasca_log_api/monitoring/metrics.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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. - -LOGS_RECEIVED_METRIC = 'log.in_logs' -"""Metrics sent with amount of logs (not requests) API receives""" - -LOGS_REJECTED_METRIC = 'log.in_logs_rejected' -"""Metric sent with amount of logs that were rejected -(i.e. invalid dimension)""" - -LOGS_BULKS_REJECTED_METRIC = 'log.in_bulks_rejected' -"""Metric sent with amount of bulk packages that were rejected due -to early stage validation (content-length, content-type). -Only valid for v3.0. -""" - -LOGS_RECEIVED_BYTE_SIZE_METRICS = 'log.in_logs_bytes' -"""Metric sent with size of payloads(a.k.a. Content-Length) - (in bytes) API receives""" - -LOGS_PROCESSING_TIME_METRIC = 'log.processing_time_ms' -"""Metric sent with time that log-api needed to process each received log. -Metric does not include time needed to authorize requests.""" - -LOGS_PUBLISHED_METRIC = 'log.out_logs' -"""Metric sent with amount of logs published to kafka""" - -LOGS_PUBLISHED_LOST_METRIC = 'log.out_logs_lost' -"""Metric sent with amount of logs that were lost due to critical error in -publish phase.""" - -LOGS_PUBLISH_TIME_METRIC = 'log.publish_time_ms' -"""Metric sent with time that publishing took""" - -LOGS_TRUNCATED_METRIC = 'log.out_logs_truncated_bytes' -"""Metric sent with amount of truncated bytes from log message""" diff --git a/monasca_log_api/policies/__init__.py b/monasca_log_api/policies/__init__.py deleted file mode 100644 index f892a22a..00000000 --- a/monasca_log_api/policies/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# Copyright 2018 OP5 AB -# -# 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 os -import pkgutil - -from oslo_config import cfg -from oslo_log import log -from oslo_utils import importutils - -from monasca_log_api.conf import role_middleware - -LOG = log.getLogger(__name__) -_BASE_MOD_PATH = 'monasca_log_api.policies.' -CONF = cfg.CONF - - -def roles_list_to_check_str(roles_list): - if roles_list: - converted_roles_list = ["role:" + role if role != '@' else role for role in roles_list] - return ' or '.join(converted_roles_list) - else: - return None - - -role_middleware.register_opts(CONF) - - -def load_policy_modules(): - """Load all modules that contain policies. - - Method iterates over modules of :py:mod:`monasca_events_api.policies` - and imports only those that contain following methods: - - - list_rules - - """ - for modname in _list_module_names(): - mod = importutils.import_module(_BASE_MOD_PATH + modname) - if hasattr(mod, 'list_rules'): - yield mod - - -def _list_module_names(): - package_path = os.path.dirname(os.path.abspath(__file__)) - for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]): - if not (modname == "opts" and ispkg): - yield modname - - -def list_rules(): - """List all policy modules rules. - - Goes through all policy modules and yields their rules - - """ - all_rules = [] - for mod in load_policy_modules(): - rules = mod.list_rules() - all_rules.extend(rules) - return all_rules diff --git a/monasca_log_api/policies/healthchecks.py b/monasca_log_api/policies/healthchecks.py deleted file mode 100644 index c5233d4b..00000000 --- a/monasca_log_api/policies/healthchecks.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2018 OP5 AB -# -# 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 oslo_config import cfg -from oslo_policy import policy - -from monasca_log_api import policies - -CHECK_AUTHORIZED_ROLES = policies.roles_list_to_check_str( - cfg.CONF.roles_middleware.check_roles) - -rules = [ - policy.DocumentedRuleDefault( - name='log_api:healthcheck:head', - check_str=CHECK_AUTHORIZED_ROLES, - description='Healthcheck head rule', - operations=[ - {'path': '/healthcheck', 'method': 'HEAD'} - ] - ), - policy.DocumentedRuleDefault( - name='log_api:healthcheck:get', - check_str=CHECK_AUTHORIZED_ROLES, - description='Healthcheck get rule', - operations=[ - {'path': '/healthcheck', 'method': 'GET'} - ] - ), -] - - -def list_rules(): - return rules diff --git a/monasca_log_api/policies/logs.py b/monasca_log_api/policies/logs.py deleted file mode 100644 index a7332e38..00000000 --- a/monasca_log_api/policies/logs.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2018 OP5 AB -# -# 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 oslo_config import cfg -from oslo_policy import policy - -from monasca_log_api import policies - -DEFAULT_AUTHORIZED_ROLES = policies.roles_list_to_check_str( - cfg.CONF.roles_middleware.default_roles) -AGENT_AUTHORIZED_ROLES = policies.roles_list_to_check_str( - cfg.CONF.roles_middleware.agent_roles) -DELEGATE_AUTHORIZED_ROLES = policies.roles_list_to_check_str( - cfg.CONF.roles_middleware.delegate_roles) - -rules = [ - policy.DocumentedRuleDefault( - name='log_api:logs:post', - check_str=' or '.join(filter(None, [AGENT_AUTHORIZED_ROLES, - DEFAULT_AUTHORIZED_ROLES, - DELEGATE_AUTHORIZED_ROLES])), - description='Logs post rule', - operations=[ - {'path': '/logs', 'method': 'POST'}, - {'path': '/log/single', 'method': 'POST'} - ] - ) -] - - -def list_rules(): - return rules diff --git a/monasca_log_api/policies/versions.py b/monasca_log_api/policies/versions.py deleted file mode 100644 index d0e7c9da..00000000 --- a/monasca_log_api/policies/versions.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2018 OP5 AB -# -# 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 oslo_config import cfg -from oslo_policy import policy - -from monasca_log_api import policies - -CHECK_AUTHORIZED_ROLES = policies.roles_list_to_check_str( - cfg.CONF.roles_middleware.check_roles) - -rules = [ - policy.DocumentedRuleDefault( - name='log_api:versions:get', - check_str=CHECK_AUTHORIZED_ROLES, - description='Versions get rule', - operations=[ - {'path': '/', 'method': 'GET'}, - {'path': '/version', 'method': 'GET'}, - {'path': '/version/{version_id}', 'method': 'GET'} - ] - ) -] - - -def list_rules(): - return rules diff --git a/monasca_log_api/tests/__init__.py b/monasca_log_api/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/monasca_log_api/tests/base.py b/monasca_log_api/tests/base.py deleted file mode 100644 index d348eafd..00000000 --- a/monasca_log_api/tests/base.py +++ /dev/null @@ -1,208 +0,0 @@ -# coding=utf-8 -# Copyright 2015 kornicameister@gmail.com -# Copyright 2015-2017 FUJITSU LIMITED -# Copyright 2018 OP5 AB -# -# 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 codecs -import os -import random -import string -from unittest import mock - -import falcon -from falcon import testing -import fixtures -from monasca_common.policy import policy_engine as policy -from oslo_config import fixture as oo_cfg -from oslo_context import fixture as oo_ctx -from oslo_serialization import jsonutils -from oslotest import base as oslotest_base -import six - -from monasca_log_api.app.base import request -from monasca_log_api import conf -from monasca_log_api import config -from monasca_log_api import policies - -policy.POLICIES = policies - - -def generate_unique_message(size): - letters = string.ascii_letters - - def rand(amount, space=True): - space = ' ' if space else '' - return ''.join((random.choice(letters + space) for _ in range(amount))) - - return rand(size) - - -def _hex_to_unicode(hex_raw): - hex_raw = six.b(hex_raw.replace(' ', '')) - hex_str_raw = codecs.getdecoder('hex')(hex_raw)[0] - hex_str = hex_str_raw.decode('utf-8', 'replace') - return hex_str - - -# NOTE(trebskit) => http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt -UNICODE_MESSAGES = [ - # Unicode is evil... - {'case': 'arabic', 'input': 'يونيكود هو الشر'}, - {'case': 'polish', 'input': 'Unicode to zło'}, - {'case': 'greek', 'input': 'Unicode είναι κακό'}, - {'case': 'portuguese', 'input': 'Unicode é malvado'}, - {'case': 'lao', 'input': 'unicode ເປັນຄວາມຊົ່ວຮ້າຍ'}, - {'case': 'german', 'input': 'Unicode ist böse'}, - {'case': 'japanese', 'input': 'ユニコードは悪です'}, - {'case': 'russian', 'input': 'Unicode - зло'}, - {'case': 'urdu', 'input': 'یونیسیڈ برائی ہے'}, - {'case': 'weird', 'input': '🆄🅽🅸🅲🅾🅳🅴 🅸🆂 🅴🆅🅸🅻...'}, # funky, huh ? - # conditions from link above - # 2.3 Other boundary conditions - {'case': 'stress_2_3_1', 'input': _hex_to_unicode('ed 9f bf')}, - {'case': 'stress_2_3_2', 'input': _hex_to_unicode('ee 80 80')}, - {'case': 'stress_2_3_3', 'input': _hex_to_unicode('ef bf bd')}, - {'case': 'stress_2_3_4', 'input': _hex_to_unicode('f4 8f bf bf')}, - {'case': 'stress_2_3_5', 'input': _hex_to_unicode('f4 90 80 80')}, - # 3.5 Impossible byes - {'case': 'stress_3_5_1', 'input': _hex_to_unicode('fe')}, - {'case': 'stress_3_5_2', 'input': _hex_to_unicode('ff')}, - {'case': 'stress_3_5_3', 'input': _hex_to_unicode('fe fe ff ff')}, - # 4.1 Examples of an overlong ASCII character - {'case': 'stress_4_1_1', 'input': _hex_to_unicode('c0 af')}, - {'case': 'stress_4_1_2', 'input': _hex_to_unicode('e0 80 af')}, - {'case': 'stress_4_1_3', 'input': _hex_to_unicode('f0 80 80 af')}, - {'case': 'stress_4_1_4', 'input': _hex_to_unicode('f8 80 80 80 af')}, - {'case': 'stress_4_1_5', 'input': _hex_to_unicode('fc 80 80 80 80 af')}, - # 4.2 Maximum overlong sequences - {'case': 'stress_4_2_1', 'input': _hex_to_unicode('c1 bf')}, - {'case': 'stress_4_2_2', 'input': _hex_to_unicode('e0 9f bf')}, - {'case': 'stress_4_2_3', 'input': _hex_to_unicode('f0 8f bf bf')}, - {'case': 'stress_4_2_4', 'input': _hex_to_unicode('f8 87 bf bf bf')}, - {'case': 'stress_4_2_5', 'input': _hex_to_unicode('fc 83 bf bf bf bf')}, - # 4.3 Overlong representation of the NUL character - {'case': 'stress_4_3_1', 'input': _hex_to_unicode('c0 80')}, - {'case': 'stress_4_3_2', 'input': _hex_to_unicode('e0 80 80')}, - {'case': 'stress_4_3_3', 'input': _hex_to_unicode('f0 80 80 80')}, - {'case': 'stress_4_3_4', 'input': _hex_to_unicode('f8 80 80 80 80')}, - {'case': 'stress_4_3_5', 'input': _hex_to_unicode('fc 80 80 80 80 80')}, - # and some cheesy example from polish novel 'Pan Tadeusz' - {'case': 'mr_t', 'input': 'Hajże na Soplicę!'}, - # it won't be complete without that one - {'case': 'mr_b', 'input': 'Grzegorz Brzęczyszczykiewicz, ' - 'Chrząszczyżewoszyce, powiat Łękołody'}, - # great success, christmas time - {'case': 'olaf', 'input': '☃'} -] - - -class DisableStatsdFixture(fixtures.Fixture): - - def setUp(self): - super(DisableStatsdFixture, self).setUp() - statsd_patch = mock.patch('monascastatsd.Connection') - statsd_patch.start() - self.addCleanup(statsd_patch.stop) - - -class ConfigFixture(oo_cfg.Config): - """Mocks configuration""" - - def __init__(self): - super(ConfigFixture, self).__init__(config.CONF) - - def setUp(self): - super(ConfigFixture, self).setUp() - self.addCleanup(self._clean_config_loaded_flag) - conf.register_opts() - self._set_defaults() - config.parse_args(argv=[]) # prevent oslo from parsing test args - - @staticmethod - def _clean_config_loaded_flag(): - config._CONF_LOADED = False - - def _set_defaults(self): - self.conf.set_default('kafka_url', '127.0.0.1', 'kafka_healthcheck') - self.conf.set_default('kafka_url', '127.0.0.1', 'log_publisher') - - -class PolicyFixture(fixtures.Fixture): - - """Override the policy with a completely new policy file. - - This overrides the policy with a completely fake and synthetic - policy file. - - """ - - def setUp(self): - super(PolicyFixture, self).setUp() - self._prepare_policy() - policy.reset() - policy.init() - - def _prepare_policy(self): - policy_dir = self.useFixture(fixtures.TempDir()) - policy_file = os.path.join(policy_dir.path, 'policy.yaml') - # load the fake_policy data and add the missing default rules. - policy_rules = jsonutils.loads('{}') - self.add_missing_default_rules(policy_rules) - with open(policy_file, 'w') as f: - jsonutils.dump(policy_rules, f) - - BaseTestCase.conf_override(policy_file=policy_file, group='oslo_policy') - BaseTestCase.conf_override(policy_dirs=[], group='oslo_policy') - - @staticmethod - def add_missing_default_rules(rules): - for rule in policies.list_rules(): - if rule.name not in rules: - rules[rule.name] = rule.check_str - - -class BaseTestCase(oslotest_base.BaseTestCase): - - def setUp(self): - super(BaseTestCase, self).setUp() - self.useFixture(ConfigFixture()) - self.useFixture(DisableStatsdFixture()) - self.useFixture(oo_ctx.ClearRequestContext()) - self.useFixture(PolicyFixture()) - - @staticmethod - def conf_override(**kw): - """Override flag variables for a test.""" - group = kw.pop('group', None) - for k, v in kw.items(): - config.CONF.set_override(k, v, group) - - @staticmethod - def conf_default(**kw): - """Override flag variables for a test.""" - group = kw.pop('group', None) - for k, v in kw.items(): - config.CONF.set_default(k, v, group) - - -class BaseApiTestCase(BaseTestCase, testing.TestCase): - - def setUp(self): - super(BaseApiTestCase, self).setUp() - self.app = falcon.API(request_type=request.Request) - # NOTE(czarneckia): Falcon 2.0.0 switches the default for this from True - # to False so we explicitly set it here to prevent the behaviour - # changing between versions. - self.app.req_options.strip_url_path_trailing_slash = True diff --git a/monasca_log_api/tests/test_config.py b/monasca_log_api/tests/test_config.py deleted file mode 100644 index 9868b66d..00000000 --- a/monasca_log_api/tests/test_config.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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 unittest import mock - -from monasca_log_api import config -from monasca_log_api.tests import base - - -class TestConfig(base.BaseTestCase): - - @mock.patch('monasca_log_api.config.sys') - def test_should_return_true_if_runs_under_gunicorn(self, sys_patch): - sys_patch.argv = [ - '/bin/gunicorn', - '--capture-output', - '--paste', - 'etc/monasca/log-api-paste.ini', - '--workers', - '1' - ] - sys_patch.executable = '/bin/python' - self.assertTrue(config._is_running_under_gunicorn()) - - @mock.patch('monasca_log_api.config.sys') - def test_should_return_false_if_runs_without_gunicorn(self, sys_patch): - sys_patch.argv = ['/bin/monasca-log-api'] - sys_patch.executable = '/bin/python' - self.assertFalse(config._is_running_under_gunicorn()) diff --git a/monasca_log_api/tests/test_healthchecks.py b/monasca_log_api/tests/test_healthchecks.py deleted file mode 100644 index 04e8819f..00000000 --- a/monasca_log_api/tests/test_healthchecks.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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 falcon -from unittest import mock - -from monasca_log_api.app.controller import healthchecks -from monasca_log_api.healthcheck import kafka_check as healthcheck -from monasca_log_api.tests import base - -ENDPOINT = '/healthcheck' - - -class TestApiHealthChecks(base.BaseApiTestCase): - - def setUp(self): - super(TestApiHealthChecks, self).setUp() - self.resource = healthchecks.HealthChecks() - self.app.add_route( - ENDPOINT, - self.resource - ) - - def test_should_return_200_for_head(self): - res = self.simulate_request( - path=ENDPOINT, - method='HEAD') - self.assertEqual(falcon.HTTP_NO_CONTENT, res.status) - - @mock.patch('monasca_log_api.healthcheck.kafka_check.KafkaHealthCheck') - def test_should_report_healthy_if_kafka_healthy(self, kafka_check): - kafka_check.healthcheck.return_value = healthcheck.CheckResult(True, - 'OK') - self.resource._kafka_check = kafka_check - - res = self.simulate_request(path=ENDPOINT, - headers={ - 'Content-Type': 'application/json' - }, - method='GET') - self.assertEqual(falcon.HTTP_OK, res.status) - - res = res.json - self.assertIn('kafka', res) - self.assertEqual('OK', res.get('kafka')) - - @mock.patch('monasca_log_api.healthcheck.kafka_check.KafkaHealthCheck') - def test_should_report_unhealthy_if_kafka_healthy(self, kafka_check): - url = 'localhost:8200' - err_str = 'Could not connect to kafka at %s' % url - kafka_check.healthcheck.return_value = healthcheck.CheckResult(False, - err_str) - self.resource._kafka_check = kafka_check - - res = self.simulate_request(path=ENDPOINT, - headers={ - 'Content-Type': 'application/json' - }, - method='GET') - self.assertEqual(falcon.HTTP_SERVICE_UNAVAILABLE, res.status) - - res = res.json - self.assertIn('kafka', res) - self.assertEqual(err_str, res.get('kafka')) diff --git a/monasca_log_api/tests/test_kafka_check.py b/monasca_log_api/tests/test_kafka_check.py deleted file mode 100644 index 2d42868b..00000000 --- a/monasca_log_api/tests/test_kafka_check.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2015-2017 FUJITSU LIMITED -# -# 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 unittest import mock - -from monasca_common.kafka_lib import client - -from monasca_log_api.healthcheck import kafka_check as kc -from monasca_log_api.tests import base - - -class KafkaCheckLogicTest(base.BaseTestCase): - - mock_kafka_url = 'localhost:1234' - mocked_topics = ['test_1', 'test_2'] - mock_config = { - 'kafka_url': mock_kafka_url, - 'kafka_topics': mocked_topics - } - - def setUp(self): - super(KafkaCheckLogicTest, self).setUp() - self.conf_default(group='kafka_healthcheck', **self.mock_config) - - @mock.patch('monasca_log_api.healthcheck.kafka_check.client.KafkaClient') - def test_should_fail_kafka_unavailable(self, kafka_client): - kafka_client.side_effect = client.KafkaUnavailableError() - kafka_health = kc.KafkaHealthCheck() - result = kafka_health.healthcheck() - - self.assertFalse(result.healthy) - - @mock.patch('monasca_log_api.healthcheck.kafka_check.client.KafkaClient') - def test_should_fail_topic_missing(self, kafka_client): - kafka = mock.Mock() - kafka.topic_partitions = [self.mocked_topics[0]] - kafka_client.return_value = kafka - - kafka_health = kc.KafkaHealthCheck() - result = kafka_health.healthcheck() - - # verify result - self.assertFalse(result.healthy) - - # ensure client was closed - self.assertTrue(kafka.close.called) - - @mock.patch('monasca_log_api.healthcheck.kafka_check.client.KafkaClient') - def test_should_pass(self, kafka_client): - kafka = mock.Mock() - kafka.topic_partitions = self.mocked_topics - kafka_client.return_value = kafka - - kafka_health = kc.KafkaHealthCheck() - result = kafka_health.healthcheck() - - self.assertTrue(result) - - # ensure client was closed - self.assertTrue(kafka.close.called) diff --git a/monasca_log_api/tests/test_log_publisher.py b/monasca_log_api/tests/test_log_publisher.py deleted file mode 100644 index f605d737..00000000 --- a/monasca_log_api/tests/test_log_publisher.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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 copy -import datetime -import random -import simplejson as json -import unittest -from unittest import mock - -from oslo_config import cfg -from oslo_log import log -import six - -from monasca_log_api.app.base import log_publisher -from monasca_log_api.app.base import model -from monasca_log_api.tests import base - -LOG = log.getLogger(__name__) -EPOCH_START = datetime.datetime(1970, 1, 1) - - -class TestSendMessage(base.BaseTestCase): - - @mock.patch('monasca_log_api.app.base.log_publisher.producer' - '.KafkaProducer') - def test_should_not_send_empty_message(self, _): - instance = log_publisher.LogPublisher() - - instance._kafka_publisher = mock.Mock() - instance.send_message({}) - - self.assertFalse(instance._kafka_publisher.publish.called) - - @unittest.expectedFailure - def test_should_not_send_message_not_dict(self): - instance = log_publisher.LogPublisher() - not_dict_value = 123 - instance.send_message(not_dict_value) - - @mock.patch('monasca_log_api.app.base.log_publisher.producer' - '.KafkaProducer') - def test_should_not_send_message_missing_keys(self, _): - # checks every combination of missing keys - # test does not rely on those keys having a value or not, - # it simply assumes that values are set but important - # message (i.e. envelope) properties are missing entirely - # that's why there are two loops instead of three - - instance = log_publisher.LogPublisher() - keys = ['log', 'creation_time', 'meta'] - - for key_1 in keys: - diff = keys[:] - diff.remove(key_1) - for key_2 in diff: - message = { - key_1: random.randint(10, 20), - key_2: random.randint(30, 50) - } - self.assertRaises(log_publisher.InvalidMessageException, - instance.send_message, - message) - - @mock.patch('monasca_log_api.app.base.log_publisher.producer' - '.KafkaProducer') - def test_should_not_send_message_missing_values(self, _): - # original message assumes that every property has value - # test modify each property one by one by removing that value - # (i.e. creating false-like value) - instance = log_publisher.LogPublisher() - message = { - 'log': { - 'message': '11' - }, - 'creation_time': 123456, - 'meta': { - 'region': 'pl' - } - } - - for key in message: - tmp_message = message - tmp_message[key] = None - self.assertRaises(log_publisher.InvalidMessageException, - instance.send_message, - tmp_message) - - @mock.patch('monasca_log_api.app.base.log_publisher.producer' - '.KafkaProducer') - def test_should_send_message(self, kafka_producer): - instance = log_publisher.LogPublisher() - instance._kafka_publisher = kafka_producer - instance.send_message({}) - - creation_time = ((datetime.datetime.utcnow() - EPOCH_START) - .total_seconds()) - application_type = 'monasca-log-api' - dimension_1_name = 'disk_usage' - dimension_1_value = '50' - dimension_2_name = 'cpu_time' - dimension_2_value = '60' - - msg = model.Envelope( - log={ - 'message': '1', - 'application_type': application_type, - 'dimensions': { - dimension_1_name: dimension_1_value, - dimension_2_name: dimension_2_value - } - }, - meta={ - 'tenantId': '1' - } - ) - msg['creation_time'] = creation_time - instance.send_message(msg) - - instance._kafka_publisher.publish.assert_called_once_with( - cfg.CONF.log_publisher.topics[0], - [json.dumps(msg, ensure_ascii=False).encode('utf-8')]) - - @mock.patch('monasca_log_api.app.base.log_publisher.producer' - '.KafkaProducer') - def test_should_send_message_multiple_topics(self, _): - topics = ['logs', 'analyzer', 'tester'] - self.conf_override(topics=topics, - max_message_size=5000, - group='log_publisher') - - instance = log_publisher.LogPublisher() - instance._kafka_publisher = mock.Mock() - instance.send_message({}) - - creation_time = ((datetime.datetime.utcnow() - EPOCH_START) - .total_seconds()) - dimension_1_name = 'disk_usage' - dimension_1_value = '50' - dimension_2_name = 'cpu_time' - dimension_2_value = '60' - application_type = 'monasca-log-api' - msg = model.Envelope( - log={ - 'message': '1', - 'application_type': application_type, - 'dimensions': { - dimension_1_name: dimension_1_value, - dimension_2_name: dimension_2_value - } - }, - meta={ - 'tenantId': '1' - } - ) - msg['creation_time'] = creation_time - json_msg = json.dumps(msg, ensure_ascii=False) - - instance.send_message(msg) - - self.assertEqual(len(topics), - instance._kafka_publisher.publish.call_count) - for topic in topics: - instance._kafka_publisher.publish.assert_any_call( - topic, - [json_msg.encode('utf-8')]) - - @mock.patch('monasca_log_api.app.base.log_publisher.producer' - '.KafkaProducer') - def test_should_send_unicode_message(self, kp): - instance = log_publisher.LogPublisher() - instance._kafka_publisher = kp - - for um in base.UNICODE_MESSAGES: - case, msg = um.values() - try: - envelope = model.Envelope( - log={ - 'message': msg, - 'application_type': 'test', - 'dimensions': { - 'test': 'test_log_publisher', - 'case': 'test_should_send_unicode_message' - } - }, - meta={ - 'tenantId': 1 - } - ) - instance.send_message(envelope) - - expected_message = json.dumps(envelope, ensure_ascii=False) - - if six.PY3: - expected_message = expected_message.encode('utf-8') - - instance._kafka_publisher.publish.assert_called_with( - cfg.CONF.log_publisher.topics[0], - [expected_message] - ) - except Exception: - LOG.exception('Failed to evaluate unicode case %s', case) - raise - - -@mock.patch( - 'monasca_log_api.app.base.log_publisher.producer' - '.KafkaProducer') -class TestTruncation(base.BaseTestCase): - EXTRA_CHARS_SIZE = len(bytearray(json.dumps({ - 'log': { - 'message': None - } - }), 'utf8')) - 2 - - def test_should_not_truncate_message_if_size_is_smaller(self, _): - diff_size = random.randint(1, 100) - self._run_truncate_test(log_size_factor=-diff_size, - truncate_by=0) - - def test_should_not_truncate_message_if_size_equal_to_max(self, _): - self._run_truncate_test(log_size_factor=0, - truncate_by=0) - - def test_should_truncate_too_big_message(self, _): - diff_size = random.randint(1, 100) - max_size = 1000 - truncate_by = ((max_size - - (max_size - log_publisher._TRUNCATED_PROPERTY_SIZE)) + - log_publisher._TRUNCATION_SAFE_OFFSET + diff_size) - self._run_truncate_test(max_message_size=1000, - log_size_factor=diff_size, - truncate_by=truncate_by) - - def _run_truncate_test(self, - max_message_size=1000, - log_size_factor=0, - truncate_by=0, - gen_fn=base.generate_unique_message): - - log_size = (max_message_size - - TestTruncation.EXTRA_CHARS_SIZE - - log_publisher._KAFKA_META_DATA_SIZE - - log_publisher._TIMESTAMP_KEY_SIZE + - log_size_factor) - - expected_log_message_size = log_size - truncate_by - - self.conf_override( - group='log_publisher', - max_message_size=max_message_size - ) - - log_msg = gen_fn(log_size) - envelope = { - 'log': { - 'message': log_msg - } - } - - instance = log_publisher.LogPublisher() - instance._logs_truncated_gauge.send = meter = mock.Mock() - - envelope_copy = copy.deepcopy(envelope) - json_envelope = instance._truncate(envelope_copy) - - parsed_envelope = json.loads(json_envelope) - - parsed_log_message = parsed_envelope['log']['message'] - parsed_log_message_len = len(parsed_log_message) - - if truncate_by > 0: - self.assertNotEqual(envelope['log']['message'], - parsed_log_message) - else: - self.assertEqual(envelope['log']['message'], - parsed_log_message) - - self.assertEqual(expected_log_message_size, parsed_log_message_len) - self.assertEqual(1, meter.call_count) - self.assertEqual(truncate_by, meter.mock_calls[0][2]['value']) diff --git a/monasca_log_api/tests/test_logs.py b/monasca_log_api/tests/test_logs.py deleted file mode 100644 index 4b38c436..00000000 --- a/monasca_log_api/tests/test_logs.py +++ /dev/null @@ -1,275 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2016 FUJITSU LIMITED -# -# 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 simplejson as json -from unittest import mock - -import falcon - -from monasca_log_api.app.base import exceptions as log_api_exceptions -from monasca_log_api.app.controller.api import headers -from monasca_log_api.app.controller.v2 import logs -from monasca_log_api.tests import base - -ROLES = 'admin' - - -def _init_resource(test): - resource = logs.Logs() - test.app.add_route('/log/single', resource) - return resource - - -class TestApiLogsVersion(base.BaseApiTestCase): - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - def test_should_return_v2_as_version(self, _, __): - logs_resource = logs.Logs() - self.assertEqual('v2.0', logs_resource.version) - - -class TestApiLogs(base.BaseApiTestCase): - - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - def test_should_contain_deprecated_details_in_successful_response(self, - _, - __): - _init_resource(self) - - res = self.simulate_request( - path='/log/single', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_DIMENSIONS.name: 'a:1', - 'Content-Type': 'application/json', - 'Content-Length': '0' - } - ) - - self.assertEqual(falcon.HTTP_204, res.status) - self.assertIn('deprecated', res.headers) - self.assertIn('link', res.headers) - - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - def test_should_fail_not_delegate_ok_cross_tenant_id(self, _, __): - _init_resource(self) - res = self.simulate_request( - path='/log/single', - method='POST', - query_string='tenant_id=1', - headers={ - 'Content-Type': 'application/json', - 'Content-Length': '0' - } - ) - self.assertEqual(falcon.HTTP_401, res.status) - - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - def test_should_pass_empty_cross_tenant_id_wrong_role(self, - log_creator, - kafka_publisher): - logs_resource = _init_resource(self) - logs_resource._log_creator = log_creator - logs_resource._kafka_publisher = kafka_publisher - - res = self.simulate_request( - path='/log/single', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_DIMENSIONS.name: 'a:1', - 'Content-Type': 'application/json', - 'Content-Length': '0' - } - ) - self.assertEqual(falcon.HTTP_204, res.status) - - self.assertEqual(1, kafka_publisher.send_message.call_count) - self.assertEqual(1, log_creator.new_log.call_count) - self.assertEqual(1, log_creator.new_log_envelope.call_count) - - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - def test_should_pass_empty_cross_tenant_id_ok_role(self, - log_creator, - kafka_publisher): - logs_resource = _init_resource(self) - logs_resource._log_creator = log_creator - logs_resource._kafka_publisher = kafka_publisher - - res = self.simulate_request( - path='/log/single', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_DIMENSIONS.name: 'a:1', - 'Content-Type': 'application/json', - 'Content-Length': '0' - } - ) - self.assertEqual(falcon.HTTP_204, res.status) - - self.assertEqual(1, kafka_publisher.send_message.call_count) - self.assertEqual(1, log_creator.new_log.call_count) - self.assertEqual(1, log_creator.new_log_envelope.call_count) - - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - def test_should_pass_delegate_cross_tenant_id_ok_role(self, - log_creator, - log_publisher): - resource = _init_resource(self) - resource._log_creator = log_creator - resource._kafka_publisher = log_publisher - - res = self.simulate_request( - path='/log/single', - method='POST', - query_string='tenant_id=1', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_DIMENSIONS.name: 'a:1', - 'Content-Type': 'application/json', - 'Content-Length': '0' - } - ) - self.assertEqual(falcon.HTTP_204, res.status) - - self.assertEqual(1, log_publisher.send_message.call_count) - self.assertEqual(1, log_creator.new_log.call_count) - self.assertEqual(1, log_creator.new_log_envelope.call_count) - - @mock.patch('monasca_log_api.common.rest.utils') - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - def test_should_fail_empty_dimensions_delegate(self, _, rest_utils): - _init_resource(self) - rest_utils.read_body.return_value = True - - res = self.simulate_request( - path='/log/single', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_DIMENSIONS.name: '', - 'Content-Type': 'application/json', - }, - body='{"message":"test"}' - ) - self.assertEqual(log_api_exceptions.HTTP_422, res.status) - - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - def test_should_fail_for_invalid_content_type(self, _, __): - _init_resource(self) - - res = self.simulate_request( - path='/log/single', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_DIMENSIONS.name: '', - 'Content-Type': 'video/3gpp', - 'Content-Length': '0' - } - ) - self.assertEqual(falcon.HTTP_415, res.status) - - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - def test_should_pass_payload_size_not_exceeded(self, _, __): - _init_resource(self) - - max_log_size = 1000 - body = json.dumps({ - 'message': 't' * (max_log_size - 100) - }) - - content_length = len(body) - self.conf_override(max_log_size=max_log_size, group='service') - - res = self.simulate_request( - path='/log/single', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_DIMENSIONS.name: '', - 'Content-Type': 'application/json', - 'Content-Length': str(content_length) - }, - body=body - ) - self.assertEqual(falcon.HTTP_204, res.status) - - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - def test_should_fail_payload_size_exceeded(self, _, __): - _init_resource(self) - - max_log_size = 1000 - content_length = max_log_size + 100 - self.conf_override(max_log_size=max_log_size, group='service') - - res = self.simulate_request( - path='/log/single', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_DIMENSIONS.name: '', - 'Content-Type': 'application/json', - 'Content-Length': str(content_length) - } - ) - self.assertEqual(falcon.HTTP_413, res.status) - - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - def test_should_fail_payload_size_equal(self, _, __): - _init_resource(self) - - max_log_size = 1000 - content_length = max_log_size - self.conf_override(max_log_size=max_log_size, group='service') - - res = self.simulate_request( - path='/log/single', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_DIMENSIONS.name: '', - 'Content-Type': 'application/json', - 'Content-Length': str(content_length) - } - ) - self.assertEqual(falcon.HTTP_413, res.status) - - @mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator') - @mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher') - def test_should_fail_content_length(self, _, __): - _init_resource(self) - - res = self.simulate_request( - path='/log/single', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_DIMENSIONS.name: '', - 'Content-Type': 'application/json' - } - ) - self.assertEqual(falcon.HTTP_411, res.status) diff --git a/monasca_log_api/tests/test_logs_v3.py b/monasca_log_api/tests/test_logs_v3.py deleted file mode 100644 index 710aa5c0..00000000 --- a/monasca_log_api/tests/test_logs_v3.py +++ /dev/null @@ -1,317 +0,0 @@ -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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 falcon -import simplejson as json -from unittest import mock - -from six import PY3 - -from monasca_log_api.app.base import exceptions as log_api_exceptions -from monasca_log_api.app.controller.api import headers -from monasca_log_api.app.controller.v3 import logs -from monasca_log_api.tests import base - -ENDPOINT = '/logs' -TENANT_ID = 'bob' -ROLES = 'admin' - - -def _init_resource(test): - resource = logs.Logs() - test.app.add_route(ENDPOINT, resource) - return resource - - -def _generate_v3_payload(log_count=None, messages=None): - if not log_count and messages: - log_count = len(messages) - v3_logs = [{ - 'message': messages[it], - 'dimensions': { - 'hostname': 'host_%d' % it, - 'component': 'component_%d' % it, - 'service': 'service_%d' % it - } - } for it in range(log_count)] - else: - v3_logs = [{ - 'message': base.generate_unique_message(100), - 'dimensions': { - 'hostname': 'host_%d' % it, - 'component': 'component_%d' % it, - 'service': 'service_%d' % it - } - } for it in range(log_count)] - v3_body = { - 'dimensions': { - 'origin': __name__ - }, - 'logs': v3_logs - } - - return v3_body, v3_logs - - -class TestApiLogsVersion(base.BaseApiTestCase): - - @mock.patch('monasca_log_api.app.controller.v3.aid' - '.bulk_processor.BulkProcessor') - def test_should_return_v3_as_version(self, _): - logs_resource = logs.Logs() - self.assertEqual('v3.0', logs_resource.version) - - -@mock.patch('monasca_log_api.app.base.log_publisher.producer.KafkaProducer') -@mock.patch('monasca_log_api.monitoring.client.monascastatsd.Connection') -class TestApiLogsMonitoring(base.BaseApiTestCase): - - def test_monitor_bulk_rejected(self, __, _): - res = _init_resource(self) - - in_counter = res._logs_in_counter.increment = mock.Mock() - bulk_counter = res._bulks_rejected_counter.increment = mock.Mock() - rejected_counter = res._logs_rejected_counter.increment = mock.Mock() - size_gauge = res._logs_size_gauge.send = mock.Mock() - - res._get_logs = mock.Mock( - side_effect=log_api_exceptions.HTTPUnprocessableEntity('')) - - log_count = 1 - v3_body, _ = _generate_v3_payload(log_count) - payload = json.dumps(v3_body) - content_length = len(payload) - - self.simulate_request( - path=ENDPOINT, - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_TENANT_ID.name: TENANT_ID, - 'Content-Type': 'application/json', - 'Content-Length': str(content_length) - }, - body=payload - ) - - self.assertEqual(1, bulk_counter.call_count) - self.assertEqual(0, in_counter.call_count) - self.assertEqual(0, rejected_counter.call_count) - self.assertEqual(0, size_gauge.call_count) - - def test_monitor_not_all_logs_ok(self, __, _): - res = _init_resource(self) - - in_counter = res._logs_in_counter.increment = mock.Mock() - bulk_counter = res._bulks_rejected_counter.increment = mock.Mock() - rejected_counter = res._logs_rejected_counter.increment = mock.Mock() - size_gauge = res._logs_size_gauge.send = mock.Mock() - - log_count = 5 - reject_logs = 1 - v3_body, _ = _generate_v3_payload(log_count) - payload = json.dumps(v3_body) - content_length = len(payload) - - side_effects = [{} for ___ in range(log_count - reject_logs)] - side_effects.append(log_api_exceptions.HTTPUnprocessableEntity('')) - - res._processor._get_dimensions = mock.Mock(side_effect=side_effects) - - self.simulate_request( - path=ENDPOINT, - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_TENANT_ID.name: TENANT_ID, - 'Content-Type': 'application/json', - 'Content-Length': str(content_length) - }, - body=payload - ) - - self.assertEqual(1, bulk_counter.call_count) - self.assertEqual(0, - bulk_counter.mock_calls[0][2]['value']) - - self.assertEqual(1, in_counter.call_count) - self.assertEqual(log_count - reject_logs, - in_counter.mock_calls[0][2]['value']) - - self.assertEqual(1, rejected_counter.call_count) - self.assertEqual(reject_logs, - rejected_counter.mock_calls[0][2]['value']) - - self.assertEqual(1, size_gauge.call_count) - self.assertEqual(content_length, - size_gauge.mock_calls[0][2]['value']) - - def test_monitor_all_logs_ok(self, __, _): - res = _init_resource(self) - - in_counter = res._logs_in_counter.increment = mock.Mock() - bulk_counter = res._bulks_rejected_counter.increment = mock.Mock() - rejected_counter = res._logs_rejected_counter.increment = mock.Mock() - size_gauge = res._logs_size_gauge.send = mock.Mock() - - res._send_logs = mock.Mock() - - log_count = 10 - - v3_body, _ = _generate_v3_payload(log_count) - - payload = json.dumps(v3_body) - content_length = len(payload) - self.simulate_request( - path=ENDPOINT, - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - headers.X_TENANT_ID.name: TENANT_ID, - 'Content-Type': 'application/json', - 'Content-Length': str(content_length) - }, - body=payload - ) - - self.assertEqual(1, bulk_counter.call_count) - self.assertEqual(0, - bulk_counter.mock_calls[0][2]['value']) - - self.assertEqual(1, in_counter.call_count) - self.assertEqual(log_count, - in_counter.mock_calls[0][2]['value']) - - self.assertEqual(1, rejected_counter.call_count) - self.assertEqual(0, - rejected_counter.mock_calls[0][2]['value']) - - self.assertEqual(1, size_gauge.call_count) - self.assertEqual(content_length, - size_gauge.mock_calls[0][2]['value']) - - -class TestApiLogs(base.BaseApiTestCase): - - @mock.patch('monasca_log_api.app.controller.v3.aid.bulk_processor.' - 'BulkProcessor') - def test_should_pass_cross_tenant_id(self, bulk_processor): - logs_resource = _init_resource(self) - logs_resource._processor = bulk_processor - - v3_body, v3_logs = _generate_v3_payload(1) - payload = json.dumps(v3_body) - content_length = len(payload) - res = self.simulate_request( - path='/logs', - method='POST', - query_string='tenant_id=1', - headers={ - headers.X_ROLES.name: ROLES, - 'Content-Type': 'application/json', - 'Content-Length': str(content_length) - }, - body=payload - ) - self.assertEqual(falcon.HTTP_204, res.status) - logs_resource._processor.send_message.assert_called_with( - logs=v3_logs, - global_dimensions=v3_body['dimensions'], - log_tenant_id='1') - - @mock.patch('monasca_log_api.app.controller.v3.aid.bulk_processor.' - 'BulkProcessor') - def test_should_fail_not_delegate_ok_cross_tenant_id(self, _): - _init_resource(self) - res = self.simulate_request( - path='/logs', - method='POST', - query_string='tenant_id=1', - headers={ - headers.X_ROLES.name: ROLES, - 'Content-Type': 'application/json', - 'Content-Length': '0' - } - ) - self.assertEqual(falcon.HTTP_400, res.status) - - @mock.patch('monasca_log_api.app.controller.v3.aid.bulk_processor.' - 'BulkProcessor') - def test_should_pass_empty_cross_tenant_id_wrong_role(self, - bulk_processor): - logs_resource = _init_resource(self) - logs_resource._processor = bulk_processor - - v3_body, _ = _generate_v3_payload(1) - payload = json.dumps(v3_body) - content_length = len(payload) - res = self.simulate_request( - path='/logs', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - 'Content-Type': 'application/json', - 'Content-Length': str(content_length) - }, - body=payload - ) - self.assertEqual(falcon.HTTP_204, res.status) - self.assertEqual(1, bulk_processor.send_message.call_count) - - @mock.patch('monasca_log_api.app.controller.v3.aid.bulk_processor.' - 'BulkProcessor') - def test_should_pass_empty_cross_tenant_id_ok_role(self, - bulk_processor): - logs_resource = _init_resource(self) - logs_resource._processor = bulk_processor - - v3_body, _ = _generate_v3_payload(1) - payload = json.dumps(v3_body) - content_length = len(payload) - res = self.simulate_request( - path='/logs', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - 'Content-Type': 'application/json', - 'Content-Length': str(content_length) - }, - body=payload - ) - self.assertEqual(falcon.HTTP_204, res.status) - self.assertEqual(1, bulk_processor.send_message.call_count) - - -class TestUnicodeLogs(base.BaseApiTestCase): - - @mock.patch('monasca_log_api.app.base.log_publisher.producer.' - 'KafkaProducer') - def test_should_send_unicode_messages(self, _): - _init_resource(self) - - messages = [m['input'] for m in base.UNICODE_MESSAGES] - v3_body, _ = _generate_v3_payload(messages=messages) - payload = json.dumps(v3_body, ensure_ascii=False) - content_length = len(payload.encode('utf8') if PY3 else payload) - res = self.simulate_request( - path='/logs', - method='POST', - headers={ - headers.X_ROLES.name: ROLES, - 'Content-Type': 'application/json', - 'Content-Length': str(content_length) - }, - body=payload - ) - self.assertEqual(falcon.HTTP_204, res.status) diff --git a/monasca_log_api/tests/test_monitoring.py b/monasca_log_api/tests/test_monitoring.py deleted file mode 100644 index 7ddef652..00000000 --- a/monasca_log_api/tests/test_monitoring.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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 unittest import mock - -from monasca_log_api.monitoring import client -from monasca_log_api.tests import base - - -class TestMonitoring(base.BaseTestCase): - - @mock.patch('monasca_log_api.monitoring.client.monascastatsd') - def test_should_use_default_dimensions_if_none_specified(self, - monascastatsd): - client.get_client() - - statsd_client = monascastatsd.Client - - expected_dimensions = client._DEFAULT_DIMENSIONS - actual_dimensions = statsd_client.call_args[1]['dimensions'] - - self.assertEqual(1, statsd_client.call_count) - self.assertEqual(expected_dimensions, actual_dimensions) - - @mock.patch('monasca_log_api.monitoring.client.monascastatsd') - def test_should_not_override_fixed_dimensions(self, - monascastatsd): - dims = { - 'service': 'foo', - 'component': 'bar' - } - - client.get_client(dims) - - statsd_client = monascastatsd.Client - - expected_dimensions = client._DEFAULT_DIMENSIONS - actual_dimensions = statsd_client.call_args[1]['dimensions'] - - self.assertEqual(1, statsd_client.call_count) - self.assertEqual(expected_dimensions, actual_dimensions) diff --git a/monasca_log_api/tests/test_policy.py b/monasca_log_api/tests/test_policy.py deleted file mode 100644 index 37d5e4e1..00000000 --- a/monasca_log_api/tests/test_policy.py +++ /dev/null @@ -1,214 +0,0 @@ -# Copyright 2016-2017 FUJITSU LIMITED -# Copyright 2018 OP5 AB -# -# 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 falcon import testing - -from monasca_common.policy import policy_engine as policy -from oslo_context import context -from oslo_policy import policy as os_policy - -from monasca_log_api.app.base import request -from monasca_log_api.policies import roles_list_to_check_str -from monasca_log_api.tests import base - - -class TestPolicyFileCase(base.BaseTestCase): - def setUp(self): - super(TestPolicyFileCase, self).setUp() - self.context = context.RequestContext(user='fake', - tenant='fake', - roles=['fake']) - self.target = {'tenant_id': 'fake'} - - def test_modified_policy_reloads(self): - tmp_file = \ - self.create_tempfiles(files=[('policies', '{}')], ext='.yaml')[0] - base.BaseTestCase.conf_override(policy_file=tmp_file, - group='oslo_policy') - - policy.reset() - policy.init() - action = 'example:test' - rule = os_policy.RuleDefault(action, '') - policy._ENFORCER.register_defaults([rule]) - - with open(tmp_file, 'w') as policy_file: - policy_file.write('{"example:test": ""}') - policy.authorize(self.context, action, self.target) - - with open(tmp_file, 'w') as policy_file: - policy_file.write('{"example:test": "!"}') - policy._ENFORCER.load_rules(True) - self.assertRaises(os_policy.PolicyNotAuthorized, policy.authorize, - self.context, action, self.target) - - -class TestPolicyCase(base.BaseTestCase): - def setUp(self): - super(TestPolicyCase, self).setUp() - rules = [ - os_policy.RuleDefault("true", "@"), - os_policy.RuleDefault("example:allowed", "@"), - os_policy.RuleDefault("example:denied", "!"), - os_policy.RuleDefault("example:lowercase_monasca_user", - "role:monasca_user or role:sysadmin"), - os_policy.RuleDefault("example:uppercase_monasca_user", - "role:MONASCA_USER or role:sysadmin"), - ] - policy.reset() - policy.init() - policy._ENFORCER.register_defaults(rules) - - def test_authorize_nonexist_action_throws(self): - action = "example:noexist" - ctx = request.Request( - testing.create_environ( - path="/", - headers={ - "X_USER_ID": "fake", - "X_PROJECT_ID": "fake", - "X_ROLES": "member" - } - ) - ) - self.assertRaises(os_policy.PolicyNotRegistered, policy.authorize, - ctx.context, action, {}) - - def test_authorize_bad_action_throws(self): - action = "example:denied" - ctx = request.Request( - testing.create_environ( - path="/", - headers={ - "X_USER_ID": "fake", - "X_PROJECT_ID": "fake", - "X_ROLES": "member" - } - ) - ) - self.assertRaises(os_policy.PolicyNotAuthorized, policy.authorize, - ctx.context, action, {}) - - def test_authorize_bad_action_no_exception(self): - action = "example:denied" - ctx = request.Request( - testing.create_environ( - path="/", - headers={ - "X_USER_ID": "fake", - "X_PROJECT_ID": "fake", - "X_ROLES": "member" - } - ) - ) - result = policy.authorize(ctx.context, action, {}, False) - self.assertFalse(result) - - def test_authorize_good_action(self): - action = "example:allowed" - ctx = request.Request( - testing.create_environ( - path="/", - headers={ - "X_USER_ID": "fake", - "X_PROJECT_ID": "fake", - "X_ROLES": "member" - } - ) - ) - result = policy.authorize(ctx.context, action, {}, False) - self.assertTrue(result) - - def test_ignore_case_role_check(self): - lowercase_action = "example:lowercase_monasca_user" - uppercase_action = "example:uppercase_monasca_user" - - monasca_user_context = request.Request( - testing.create_environ( - path="/", - headers={ - "X_USER_ID": "monasca_user", - "X_PROJECT_ID": "fake", - "X_ROLES": "MONASCA_user" - } - ) - ) - self.assertTrue(policy.authorize(monasca_user_context.context, - lowercase_action, - {})) - self.assertTrue(policy.authorize(monasca_user_context.context, - uppercase_action, - {})) - - -class RegisteredPoliciesTestCase(base.BaseTestCase): - def __init__(self, *args, **kwds): - super(RegisteredPoliciesTestCase, self).__init__(*args, **kwds) - self.default_roles = ['monasca-user', 'admin'] - - def test_healthchecks_policies_roles(self): - healthcheck_policies = { - 'log_api:healthcheck:head': ['any_role'], - 'log_api:healthcheck:get': ['any_role'] - } - - self._assert_rules(healthcheck_policies) - - def test_versions_policies_roles(self): - versions_policies = { - 'log_api:versions:get': ['any_role'] - } - - self._assert_rules(versions_policies) - - def test_logs_policies_roles(self): - - logs_policies = { - 'log_api:logs:post': self.default_roles - } - - self._assert_rules(logs_policies) - - def _assert_rules(self, policies_list): - for policy_name in policies_list: - registered_rule = policy.get_rules()[policy_name] - if hasattr(registered_rule, 'rules'): - self.assertEqual(len(registered_rule.rules), - len(policies_list[policy_name])) - for role in policies_list[policy_name]: - ctx = self._get_request_context(role) - self.assertTrue(policy.authorize(ctx.context, - policy_name, - {}) - ) - - @staticmethod - def _get_request_context(role): - return request.Request( - testing.create_environ( - path='/', - headers={'X_ROLES': role} - ) - ) - - -class PolicyUtilsTestCase(base.BaseTestCase): - def test_roles_list_to_check_str(self): - self.assertEqual(roles_list_to_check_str(['test_role']), 'role:test_role') - self.assertEqual(roles_list_to_check_str(['role1', 'role2', 'role3']), - 'role:role1 or role:role2 or role:role3') - self.assertEqual(roles_list_to_check_str(['@']), '@') - self.assertEqual(roles_list_to_check_str(['role1', '@', 'role2']), - 'role:role1 or @ or role:role2') - self.assertIsNone(roles_list_to_check_str(None)) diff --git a/monasca_log_api/tests/test_request.py b/monasca_log_api/tests/test_request.py deleted file mode 100644 index 0467fee9..00000000 --- a/monasca_log_api/tests/test_request.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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 unittest import mock - -from falcon import testing - -from monasca_log_api.app.base import request -from monasca_log_api.app.base import validation -from monasca_log_api.tests import base - - -class TestRequest(base.BaseTestCase): - - def test_use_context_from_request(self): - req = request.Request( - testing.create_environ( - path='/', - headers={ - 'X_AUTH_TOKEN': '111', - 'X_USER_ID': '222', - 'X_PROJECT_ID': '333', - 'X_ROLES': 'terminator,predator' - } - ) - ) - - self.assertEqual('111', req.context.auth_token) - self.assertEqual('222', req.user_id) - self.assertEqual('333', req.project_id) - self.assertEqual(['terminator', 'predator'], req.roles) - - def test_validate_context_type(self): - with mock.patch.object(validation, - 'validate_content_type') as vc_type, \ - mock.patch.object(validation, - 'validate_payload_size') as vp_size, \ - mock.patch.object(validation, - 'validate_cross_tenant') as vc_tenant: - req = request.Request(testing.create_environ()) - vc_type.side_effect = Exception() - - try: - req.validate(['test']) - except Exception as ex: - self.assertEqual(1, vc_type.call_count) - self.assertEqual(0, vp_size.call_count) - self.assertEqual(0, vc_tenant.call_count) - - self.assertIsInstance(ex, Exception) - - def test_validate_payload_size(self): - with mock.patch.object(validation, - 'validate_content_type') as vc_type, \ - mock.patch.object(validation, - 'validate_payload_size') as vp_size, \ - mock.patch.object(validation, - 'validate_cross_tenant') as vc_tenant: - - req = request.Request(testing.create_environ()) - vp_size.side_effect = Exception() - - try: - req.validate(['test']) - except Exception as ex: - self.assertEqual(1, vc_type.call_count) - self.assertEqual(1, vp_size.call_count) - self.assertEqual(0, vc_tenant.call_count) - - self.assertIsInstance(ex, Exception) - - def test_validate_cross_tenant(self): - with mock.patch.object(validation, - 'validate_content_type') as vc_type, \ - mock.patch.object(validation, - 'validate_payload_size') as vp_size, \ - mock.patch.object(validation, - 'validate_cross_tenant') as vc_tenant: - - req = request.Request(testing.create_environ()) - vc_tenant.side_effect = Exception() - - try: - req.validate(['test']) - except Exception as ex: - self.assertEqual(1, vc_type.call_count) - self.assertEqual(1, vp_size.call_count) - self.assertEqual(1, vc_tenant.call_count) - - self.assertIsInstance(ex, Exception) diff --git a/monasca_log_api/tests/test_role_middleware.py b/monasca_log_api/tests/test_role_middleware.py deleted file mode 100644 index 59e54f28..00000000 --- a/monasca_log_api/tests/test_role_middleware.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2015-2017 FUJITSU LIMITED -# -# 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 unittest import mock - -from webob import response - -from monasca_log_api.middleware import role_middleware as rm -from monasca_log_api.tests import base - - -class SideLogicTestEnsureLowerRoles(base.BaseTestCase): - - def test_should_ensure_lower_roles(self): - roles = ['CMM-Admin', ' CmM-User '] - expected = ['cmm-admin', 'cmm-user'] - self.assertItemsEqual(expected, rm._ensure_lower_roles(roles)) - - def test_should_return_empty_array_for_falsy_input_1(self): - roles = [] - expected = [] - self.assertItemsEqual(expected, rm._ensure_lower_roles(roles)) - - def test_should_return_empty_array_for_falsy_input_2(self): - roles = None - expected = [] - self.assertItemsEqual(expected, rm._ensure_lower_roles(roles)) - - -class SideLogicTestIntersect(base.BaseTestCase): - - def test_should_intersect_seqs(self): - seq_1 = [1, 2, 3] - seq_2 = [2] - - expected = [2] - - self.assertItemsEqual(expected, rm._intersect(seq_1, seq_2)) - self.assertItemsEqual(expected, rm._intersect(seq_2, seq_1)) - - def test_should_intersect_empty(self): - seq_1 = [] - seq_2 = [] - - expected = [] - - self.assertItemsEqual(expected, rm._intersect(seq_1, seq_2)) - self.assertItemsEqual(expected, rm._intersect(seq_2, seq_1)) - - def test_should_not_intersect_without_common_elements(self): - seq_1 = [1, 2, 3] - seq_2 = [4, 5, 6] - - expected = [] - - self.assertItemsEqual(expected, rm._intersect(seq_1, seq_2)) - self.assertItemsEqual(expected, rm._intersect(seq_2, seq_1)) - - -class RolesMiddlewareSideLogicTest(base.BaseTestCase): - - def test_should_apply_middleware_for_valid_path(self): - paths = ['/', '/v2.0/', '/v2.0/log/'] - - instance = rm.RoleMiddleware(None) - instance._path = paths - - for p in paths: - req = mock.Mock() - req.method = 'GET' - req.path = p - self.assertTrue(instance._can_apply_middleware(req)) - - def test_should_apply_middleware_for_invalid_path(self): - paths = ['/v2.0/', '/v2.0/log/'] - - instance = rm.RoleMiddleware(None) - instance._path = paths - - for p in paths: - pp = 'test/%s' % p - req = mock.Mock() - req.method = 'GET' - req.path = pp - self.assertFalse(instance._can_apply_middleware(req)) - - def test_should_reject_OPTIONS_request(self): - instance = rm.RoleMiddleware(None) - req = mock.Mock() - req.method = 'OPTIONS' - req.path = '/' - self.assertFalse(instance._can_apply_middleware(req)) - - def test_should_return_true_if_authenticated(self): - instance = rm.RoleMiddleware(None) - - req = mock.Mock() - req.headers = {rm._X_IDENTITY_STATUS: rm._CONFIRMED_STATUS} - - self.assertTrue(instance._is_authenticated(req)) - - def test_should_return_false_if_not_authenticated(self): - instance = rm.RoleMiddleware(None) - - req = mock.Mock() - req.headers = {rm._X_IDENTITY_STATUS: 'Some_Other_Status'} - - self.assertFalse(instance._is_authenticated(req)) - - def test_should_return_false_if_identity_status_not_found(self): - instance = rm.RoleMiddleware(None) - - req = mock.Mock() - req.headers = {} - - self.assertFalse(instance._is_authenticated(req)) - - def test_should_return_true_if_is_agent(self): - roles = 'cmm-admin,cmm-user' - roles_array = roles.split(',') - - default_roles = [roles_array[0]] - admin_roles = [roles_array[1]] - - instance = rm.RoleMiddleware(None) - instance._default_roles = default_roles - instance._agent_roles = admin_roles - - req = mock.Mock() - req.headers = {rm._X_ROLES: roles} - - is_agent = instance._is_agent(req) - - self.assertTrue(is_agent) - - -class RolesMiddlewareLogicTest(base.BaseTestCase): - - def test_not_process_further_if_cannot_apply_path(self): - roles = 'cmm-admin,cmm-user' - roles_array = roles.split(',') - - default_roles = [roles_array[0]] - admin_roles = [roles_array[1]] - - instance = rm.RoleMiddleware(None) - instance._default_roles = default_roles - instance._agent_roles = admin_roles - instance._path = ['/test'] - - # spying - instance._is_authenticated = mock.Mock() - instance._is_agent = mock.Mock() - - req = mock.Mock() - req.headers = {rm._X_ROLES: roles} - req.path = '/different/test' - - instance.process_request(req=req) - - self.assertFalse(instance._is_authenticated.called) - self.assertFalse(instance._is_agent.called) - - def test_not_process_further_if_cannot_apply_method(self): - roles = 'cmm-admin,cmm-user' - roles_array = roles.split(',') - - default_roles = [roles_array[0]] - admin_roles = [roles_array[1]] - - instance = rm.RoleMiddleware(None) - instance._default_roles = default_roles - instance._agent_roles = admin_roles - instance._path = ['/test'] - - # spying - instance._is_authenticated = mock.Mock() - instance._is_agent = mock.Mock() - - req = mock.Mock() - req.headers = {rm._X_ROLES: roles} - req.path = '/test' - req.method = 'OPTIONS' - - instance.process_request(req=req) - - self.assertFalse(instance._is_authenticated.called) - self.assertFalse(instance._is_agent.called) - - def test_should_produce_json_response_if_not_authenticated( - self): - instance = rm.RoleMiddleware(None) - is_agent = True - is_authenticated = False - - instance._can_apply_middleware = mock.Mock(return_value=True) - instance._is_agent = mock.Mock(return_value=is_agent) - instance._is_authenticated = mock.Mock(return_value=is_authenticated) - - req = mock.Mock() - req.environ = {} - req.headers = { - 'X-Tenant-Id': '11111111' - } - - result = instance.process_request(req=req) - - self.assertIsNotNone(result) - self.assertIsInstance(result, response.Response) - - status = result.status_code - json_body = result.json_body - message = json_body.get('message') - - self.assertIn('Failed to authenticate request for', message) - self.assertEqual(401, status) diff --git a/monasca_log_api/tests/test_service.py b/monasca_log_api/tests/test_service.py deleted file mode 100644 index 54b1d4c0..00000000 --- a/monasca_log_api/tests/test_service.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright 2015 kornicameister@gmail.com -# Copyright 2016-2017 FUJITSU LIMITED -# -# 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 datetime -import unittest -from unittest import mock - -from falcon import errors -from falcon import testing - -from monasca_log_api.app.base import exceptions -from monasca_log_api.app.base import validation -from monasca_log_api.app.controller.v2.aid import service as aid_service -from monasca_log_api.tests import base - - -class IsDelegate(base.BaseTestCase): - - def __init__(self, *args, **kwargs): - super(IsDelegate, self).__init__(*args, **kwargs) - self._roles = ['admin'] - - def test_is_delegate_ok_role(self): - self.assertTrue(validation.validate_is_delegate(self._roles)) - - def test_is_delegate_ok_role_in_roles(self): - self._roles.extend(['a_role', 'b_role']) - self.assertTrue(validation.validate_is_delegate(self._roles)) - - def test_is_delegate_not_ok_role(self): - roles = ['a_role', 'b_role'] - self.assertFalse(validation.validate_is_delegate(roles)) - - -class ParseDimensions(base.BaseTestCase): - def test_should_fail_for_empty_dimensions(self): - self.assertRaises(exceptions.HTTPUnprocessableEntity, - aid_service.parse_dimensions, - '') - self.assertRaises(exceptions.HTTPUnprocessableEntity, - aid_service.parse_dimensions, - None) - - def test_should_fail_for_empty_dim_in_dimensions(self): - err = self.assertRaises(exceptions.HTTPUnprocessableEntity, - aid_service.parse_dimensions, - ',') - self.assertEqual(err.description, 'Dimension cannot be empty') - - def test_should_fail_for_invalid_dim_in_dimensions(self): - invalid_dim = 'a' - err = self.assertRaises(exceptions.HTTPUnprocessableEntity, - aid_service.parse_dimensions, - invalid_dim) - self.assertEqual(err.description, '%s is not a valid dimension' - % invalid_dim) - - def test_should_pass_for_valid_dimensions(self): - dimensions = 'a:1,b:2' - expected = { - 'a': '1', - 'b': '2' - } - - self.assertDictEqual(expected, - aid_service.parse_dimensions(dimensions)) - - -class ParseApplicationType(base.BaseTestCase): - def test_should_return_none_for_none(self): - self.assertIsNone(aid_service.parse_application_type(None)) - - def test_should_return_none_for_empty(self): - self.assertIsNone(aid_service.parse_application_type('')) - - def test_should_return_none_for_whitespace_filled(self): - self.assertIsNone(aid_service.parse_application_type(' ')) - - def test_should_return_value_for_ok_value(self): - app_type = 'monasca' - self.assertEqual(app_type, - aid_service.parse_application_type(app_type)) - - def test_should_return_value_for_ok_value_with_spaces(self): - app_type = ' monasca ' - expected = 'monasca' - self.assertEqual(expected, - aid_service.parse_application_type(app_type)) - - -class ApplicationTypeValidations(base.BaseTestCase): - def test_should_pass_for_empty_app_type(self): - validation.validate_application_type() - validation.validate_application_type('') - - def test_should_fail_for_invalid_length(self): - r_app_type = testing.rand_string(300, 600) - err = self.assertRaises(exceptions.HTTPUnprocessableEntity, - validation.validate_application_type, - r_app_type) - - length = validation.APPLICATION_TYPE_CONSTRAINTS['MAX_LENGTH'] - msg = ('Application type {type} must be ' - '{length} characters or less'.format(type=r_app_type, - length=length)) - - self.assertEqual(err.description, msg) - - def test_should_fail_for_invalid_content(self): - r_app_type = '%#$@!' - - err = self.assertRaises(exceptions.HTTPUnprocessableEntity, - validation.validate_application_type, - r_app_type) - msg = ('Application type %s may only contain: "a-z A-Z 0-9 _ - ."' % - r_app_type) - self.assertEqual(err.description, msg) - - def test_should_pass_for_ok_app_type(self): - r_app_type = 'monasca' - validation.validate_application_type(r_app_type) - - -class DimensionsValidations(base.BaseTestCase): - @unittest.expectedFailure - def test_should_fail_for_none_dimensions(self): - validation.validate_dimensions(None) - - @unittest.expectedFailure - def test_should_fail_pass_for_non_iterable_dimensions_str(self): - validation.validate_dimensions('') - - @unittest.expectedFailure - def test_should_fail_pass_for_non_iterable_dimensions_number(self): - validation.validate_dimensions(1) - - def test_should_pass_for_empty_dimensions_array(self): - validation.validate_dimensions({}) - - def test_should_fail_too_empty_name(self): - dimensions = {'': 1} - - err = self.assertRaises(exceptions.HTTPUnprocessableEntity, - validation.validate_dimensions, - dimensions) - msg = 'Dimension name cannot be empty' - self.assertEqual(err.description, msg) - - def test_should_fail_too_long_name(self): - name = testing.rand_string(256, 260) - dimensions = {name: 1} - - err = self.assertRaises(exceptions.HTTPUnprocessableEntity, - validation.validate_dimensions, - dimensions) - msg = 'Dimension name %s must be 255 characters or less' % name - self.assertEqual(err.description, msg) - - def test_should_fail_underscore_at_begin(self): - name = '_aDim' - dimensions = {name: 1} - - err = self.assertRaises(exceptions.HTTPUnprocessableEntity, - validation.validate_dimensions, - dimensions) - msg = 'Dimension name %s cannot start with underscore (_)' % name - self.assertEqual(err.description, msg) - - def test_should_fail_invalid_chars(self): - name = '<>' - dimensions = {name: 1} - - err = self.assertRaises(exceptions.HTTPUnprocessableEntity, - validation.validate_dimensions, - dimensions) - invalid_chars = '> < = { } ( ) \' " , ; &' - msg = 'Dimension name %s may not contain: %s' % (name, invalid_chars) - self.assertEqual(err.description, msg) - - def test_should_fail_ok_name_empty_value(self): - name = 'monasca' - dimensions = {name: ''} - - err = self.assertRaises(exceptions.HTTPUnprocessableEntity, - validation.validate_dimensions, - dimensions) - msg = 'Dimension value cannot be empty' - self.assertEqual(err.description, msg) - - def test_should_fail_ok_name_too_long_value(self): - name = 'monasca' - value = testing.rand_string(256, 300) - dimensions = {name: value} - - err = self.assertRaises(exceptions.HTTPUnprocessableEntity, - validation.validate_dimensions, - dimensions) - msg = 'Dimension value %s must be 255 characters or less' % value - self.assertEqual(err.description, msg) - - def test_should_pass_ok_name_ok_value_empty_service(self): - name = 'monasca' - value = '1' - dimensions = {name: value} - validation.validate_dimensions(dimensions) - - def test_should_pass_ok_name_ok_value_service_SERVICE_DIMENSIONS_as_name( - self): - name = 'some_name' - value = '1' - dimensions = {name: value} - validation.validate_dimensions(dimensions) - - -class ContentTypeValidations(base.BaseTestCase): - def test_should_pass_text_plain(self): - content_type = 'text/plain' - allowed_types = ['text/plain'] - - req = mock.Mock() - req.content_type = content_type - validation.validate_content_type(req, allowed_types) - - def test_should_pass_application_json(self): - content_type = 'application/json' - allowed_types = ['application/json'] - - req = mock.Mock() - req.content_type = content_type - - validation.validate_content_type(req, allowed_types) - - def test_should_fail_invalid_content_type(self): - content_type = 'no/such/type' - allowed_types = ['application/json'] - - req = mock.Mock() - req.content_type = content_type - - self.assertRaises( - errors.HTTPUnsupportedMediaType, - validation.validate_content_type, - req, - allowed_types - ) - - def test_should_fail_missing_header(self): - content_type = None - allowed_types = ['application/json'] - - req = mock.Mock() - req.content_type = content_type - - self.assertRaises( - errors.HTTPMissingHeader, - validation.validate_content_type, - req, - allowed_types - ) - - -class PayloadSizeValidations(base.BaseTestCase): - - def test_should_fail_missing_header(self): - content_length = None - req = mock.Mock() - req.content_length = content_length - self.assertRaises( - errors.HTTPLengthRequired, - validation.validate_payload_size, - req - ) - - def test_should_pass_limit_not_exceeded(self): - content_length = 120 - max_log_size = 240 - self.conf_override(max_log_size=max_log_size, - group='service') - - req = mock.Mock() - req.content_length = content_length - - validation.validate_payload_size(req) - - def test_should_fail_limit_exceeded(self): - content_length = 120 - max_log_size = 60 - self.conf_override(max_log_size=max_log_size, - group='service') - - req = mock.Mock() - req.content_length = content_length - - self.assertRaises( - errors.HTTPPayloadTooLarge, - validation.validate_payload_size, - req - ) - - def test_should_fail_limit_equal(self): - content_length = 120 - max_log_size = 120 - self.conf_override(max_log_size=max_log_size, - group='service') - - req = mock.Mock() - req.content_length = content_length - - self.assertRaises( - errors.HTTPPayloadTooLarge, - validation.validate_payload_size, - req - ) - - -class LogMessageValidations(base.BaseTestCase): - def test_should_pass_message_in_log_property(self): - log_object = { - 'message': 'some messages', - 'application_type': 'monasca-log-api', - 'dimensions': { - 'hostname': 'devstack' - } - } - validation.validate_log_message(log_object) - - @unittest.expectedFailure - def test_should_fail_pass_for_non_message_in_log_property(self): - log_object = { - 'massage': 'some messages', - 'application_type': 'monasca-log-api', - 'dimensions': { - 'hostname': 'devstack' - } - } - validation.validate_log_message(log_object) - - def test_should_fail_with_empty_message(self): - self.assertRaises(exceptions.HTTPUnprocessableEntity, - validation.validate_log_message, {}) - - -class LogsCreatorNewLog(base.BaseTestCase): - def setUp(self): - super(LogsCreatorNewLog, self).setUp() - self.instance = aid_service.LogCreator() - - def test_should_create_log_from_json(self): - msg = u'Hello World' - path = u'/var/log/messages' - payload = {"path": path, - "message": msg} - app_type = 'monasca' - dimensions = 'cpu_time:30' - - expected_log = { - 'message': msg, - 'dimensions': { - 'component': app_type, - 'cpu_time': '30' - }, - 'path': path - } - - self.assertEqual(expected_log, self.instance.new_log( - application_type=app_type, - dimensions=dimensions, - payload=payload - )) - - def test_should_create_log_from_text(self): - msg = u'Hello World' - app_type = 'monasca' - dimension_name = 'cpu_time' - dimension_value = 30 - dimensions = '%s:%s' % (dimension_name, str(dimension_value)) - - expected_log = { - 'message': msg, - 'dimensions': { - 'component': app_type, - dimension_name: str(dimension_value) - } - } - - self.assertEqual(expected_log, self.instance.new_log( - application_type=app_type, - dimensions=dimensions, - payload=msg, - content_type='text/plain' - )) - - -class LogCreatorNewEnvelope(base.BaseTestCase): - def setUp(self): - super(LogCreatorNewEnvelope, self).setUp() - self.instance = aid_service.LogCreator() - - def test_should_create_envelope(self): - msg = u'Hello World' - path = u'/var/log/messages' - app_type = 'monasca' - dimension_name = 'cpu_time' - dimension_value = 30 - expected_log = { - 'message': msg, - 'application_type': app_type, - 'dimensions': { - dimension_name: str(dimension_value) - }, - 'path': path - } - tenant_id = 'a_tenant' - none = None - meta = {'tenantId': tenant_id, 'region': none} - timestamp = (datetime.datetime.utcnow() - - datetime.datetime(1970, 1, 1)).total_seconds() - expected_envelope = { - 'log': expected_log, - 'creation_time': timestamp, - 'meta': meta - } - - with mock.patch.object(self.instance, '_create_meta_info', - return_value=meta): - actual_envelope = self.instance.new_log_envelope(expected_log, - tenant_id) - - self.assertEqual(expected_envelope.get('log'), - actual_envelope.get('log')) - self.assertEqual(expected_envelope.get('meta'), - actual_envelope.get('meta')) - self.assertDictEqual( - expected_envelope.get('log').get('dimensions'), - actual_envelope.get('log').get('dimensions')) - - @unittest.expectedFailure - def test_should_not_create_log_none(self): - log_object = None - tenant_id = 'a_tenant' - - self.instance.new_log_envelope(log_object, tenant_id) - - @unittest.expectedFailure - def test_should_not_create_log_empty(self): - log_object = {} - tenant_id = 'a_tenant' - - self.instance.new_log_envelope(log_object, tenant_id) - - @unittest.expectedFailure - def test_should_not_create_tenant_none(self): - log_object = { - 'message': '' - } - tenant_id = None - - self.instance.new_log_envelope(log_object, tenant_id) - - @unittest.expectedFailure - def test_should_not_create_tenant_empty(self): - log_object = { - 'message': '' - } - tenant_id = '' - - self.instance.new_log_envelope(log_object, tenant_id) diff --git a/monasca_log_api/tests/test_v2_v3_compare.py b/monasca_log_api/tests/test_v2_v3_compare.py deleted file mode 100644 index 57c78ca1..00000000 --- a/monasca_log_api/tests/test_v2_v3_compare.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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 simplejson as json -from unittest import mock - -from monasca_log_api.app.controller.api import headers -from monasca_log_api.app.controller.v2 import logs as v2_logs -from monasca_log_api.app.controller.v3 import logs as v3_logs -from monasca_log_api.tests import base - - -class TestApiSameV2V3Output(base.BaseApiTestCase): - - # noinspection PyProtectedMember - @mock.patch('monasca_log_api.app.base.log_publisher.' - 'producer.KafkaProducer') - def test_send_identical_messages(self, _): - # mocks only log publisher, so the last component that actually - # sends data to kafka - # case is to verify if publisher was called with same arguments - # for both cases - - v2 = v2_logs.Logs() - v3 = v3_logs.Logs() - - publish_mock = mock.Mock() - - v2._kafka_publisher._kafka_publisher.publish = publish_mock - v3._processor._kafka_publisher.publish = publish_mock - - component = 'monasca-log-api' - service = 'laas' - hostname = 'kornik' - tenant_id = 'ironMan' - roles = 'admin' - - v2_dimensions = 'hostname:%s,service:%s' % (hostname, service) - v3_dimensions = { - 'hostname': hostname, - 'component': component, - 'service': service - } - - v2_body = { - 'message': 'test' - } - - v3_body = { - 'logs': [ - { - 'message': 'test', - 'dimensions': v3_dimensions - } - ] - } - - self.app.add_route('/v2.0', v2) - self.app.add_route('/v3.0', v3) - - self.simulate_request( - path='/v2.0', - method='POST', - headers={ - headers.X_ROLES.name: roles, - headers.X_DIMENSIONS.name: v2_dimensions, - headers.X_APPLICATION_TYPE.name: component, - headers.X_TENANT_ID.name: tenant_id, - 'Content-Type': 'application/json', - }, - body=json.dumps(v2_body) - ) - - self.simulate_request( - path='/v3.0', - method='POST', - headers={ - headers.X_ROLES.name: roles, - headers.X_TENANT_ID.name: tenant_id, - 'Content-Type': 'application/json', - }, - body=json.dumps(v3_body) - ) - - self.assertEqual(2, publish_mock.call_count) - - # in v2 send_messages is called with single envelope - v2_send_msg_arg = publish_mock.mock_calls[0][1][1] - - # in v3 it is always called with list of envelopes - v3_send_msg_arg = publish_mock.mock_calls[1][1][1] - - self.maxDiff = None - - # at this point we know that both args should be identical - self.assertEqual(type(v2_send_msg_arg), type(v3_send_msg_arg)) - self.assertIsInstance(v3_send_msg_arg, list) - - self.assertEqual(len(v2_send_msg_arg), len(v3_send_msg_arg)) - self.assertEqual(1, len(v2_send_msg_arg)) - - v2_msg_as_dict = json.loads(v2_send_msg_arg[0]) - v3_msg_as_dict = json.loads(v3_send_msg_arg[0]) - - self.assertDictEqual(v2_msg_as_dict, v3_msg_as_dict) diff --git a/monasca_log_api/tests/test_version.py b/monasca_log_api/tests/test_version.py deleted file mode 100644 index 4d7605b4..00000000 --- a/monasca_log_api/tests/test_version.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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 monasca_log_api.tests import base -from monasca_log_api import version - - -class TestAppVersion(base.BaseTestCase): - def test_should_report_version(self): - self.assertIsNotNone(version.version_str) diff --git a/monasca_log_api/tests/test_versions.py b/monasca_log_api/tests/test_versions.py deleted file mode 100644 index 8a450705..00000000 --- a/monasca_log_api/tests/test_versions.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright 2016 FUJITSU LIMITED -# -# 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 falcon - -from monasca_log_api.app.controller import versions -from monasca_log_api.tests import base - - -def _get_versioned_url(version_id): - return '/version/%s' % version_id - - -class TestApiVersions(base.BaseApiTestCase): - - def setUp(self): - super(TestApiVersions, self).setUp() - self.versions = versions.Versions() - self.app.add_route("/", self.versions) - self.app.add_route("/version/", self.versions) - self.app.add_route("/version/{version_id}", self.versions) - - def test_should_fail_for_unsupported_version(self): - unsupported_version = 'v5.0' - uri = _get_versioned_url(unsupported_version) - - res = self.simulate_request( - path=uri, - method='GET', - headers={ - 'Content-Type': 'application/json' - } - ) - - self.assertEqual(falcon.HTTP_400, res.status) - - def test_should_return_all_supported_versions(self): - - def _check_elements(): - self.assertIn('elements', response) - elements = response.get('elements') - self.assertIsInstance(elements, list) - - for el in elements: - # do checkup by expected keys - self.assertIn('id', el) - self.assertItemsEqual([ - u'id', - u'links', - u'status', - u'updated' - ], el.keys()) - - ver = el.get('id') - self.assertIn(ver, expected_versions) - - def _check_global_links(): - self.assertIn('links', response) - links = response.get('links') - self.assertIsInstance(links, list) - - for link in links: - self.assertIn('rel', link) - key = link.get('rel') - self.assertIn(key, expected_links_keys) - href = link.get('href') - self.assertTrue(href.startswith(expected_url)) - - expected_versions = 'v2.0', 'v3.0' - expected_links_keys = 'self', 'version', 'healthcheck' - expected_protocol = 'http' - expected_host = 'fakehost.com' - expected_url = '{}://{}'.format(expected_protocol, expected_host) - - for expected_path in ['/', '/version']: - res = self.simulate_request( - path=expected_path, - protocol=expected_protocol, - host=expected_host, - method='GET', - headers={ - 'Content-Type': 'application/json' - } - ) - self.assertEqual(falcon.HTTP_200, res.status) - - response = res.json - - _check_elements() - _check_global_links() - - def test_should_return_expected_version_id(self): - expected_versions = 'v2.0', 'v3.0' - for expected_version in expected_versions: - uri = _get_versioned_url(expected_version) - res = self.simulate_request( - path=uri, - method='GET', - headers={ - 'Content-Type': 'application/json' - }, - ) - self.assertEqual(falcon.HTTP_200, res.status) - - response = res.json - self.assertIn('elements', response) - self.assertIn('links', response) - - elements = response.get('elements') - self.assertIsInstance(elements, list) - self.assertEqual(1, len(elements)) - - el = elements[0] - ver = el.get('id') - self.assertEqual(expected_version, ver) diff --git a/monasca_log_api/version.py b/monasca_log_api/version.py deleted file mode 100644 index 7b20ca39..00000000 --- a/monasca_log_api/version.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2017 FUJITSU LIMITED -# -# 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 pbr.version - -version_info = pbr.version.VersionInfo('monasca-log-api') -version_str = version_info.version_string() diff --git a/playbooks/docker-publish.yml b/playbooks/docker-publish.yml deleted file mode 100644 index f9429f22..00000000 --- a/playbooks/docker-publish.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -- hosts: all - tasks: - - name: Login to Dockerhub - command: "docker login -u {{ doker_hub_login_log_api.user }} -p {{ doker_hub_login_log_api.password }}" - no_log: true - - - name: List images - shell: "docker images --format '{% raw %}{{ .Repository }}:{{ .Tag }}{% endraw %}' | grep monasca" - - - name: Push to Docker Hub all tags - shell: "docker push monasca/log-api" diff --git a/releasenotes/notes/cli_args-6dc5e2d13337b871.yaml b/releasenotes/notes/cli_args-6dc5e2d13337b871.yaml deleted file mode 100644 index b25af046..00000000 --- a/releasenotes/notes/cli_args-6dc5e2d13337b871.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -other: - - | - Enabled possibility of specifying the CLI arguments, when launching - monasca-log-api, for cases where API is not deployed using Gunicorn - server. diff --git a/releasenotes/notes/deprecate-60181d946200dff7.yaml b/releasenotes/notes/deprecate-60181d946200dff7.yaml deleted file mode 100644 index 3477621b..00000000 --- a/releasenotes/notes/deprecate-60181d946200dff7.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -deprecations: - - | - This API is deprecated. Last maintained release is OpenStack Train. - Please use monasca-api for newer versions. This repository is kept only - for maintenance purposes. diff --git a/releasenotes/notes/drop-py-2-7-02f3af38da6f6634.yaml b/releasenotes/notes/drop-py-2-7-02f3af38da6f6634.yaml deleted file mode 100644 index 882a169e..00000000 --- a/releasenotes/notes/drop-py-2-7-02f3af38da6f6634.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -upgrade: - - | - Python 2.7 support has been dropped. Last release of monasca-log-api - to support python 2.7 is OpenStack Train. The minimum version of Python now - supported by monasca-log-api is Python 3.6. diff --git a/releasenotes/notes/os-docs-bf79803595ac884b.yaml b/releasenotes/notes/os-docs-bf79803595ac884b.yaml deleted file mode 100644 index 1ec3efc7..00000000 --- a/releasenotes/notes/os-docs-bf79803595ac884b.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- - -deprecations: - - Usage of Markdown based documentation has been deprecated. - It will not be maintained and in future removed from the project - -upgrade: - - Documentation handling of monasca-log-api has been migrated - to match OpenStack process diff --git a/releasenotes/notes/oslo-policy-e142fa9243a8dcf6.yaml b/releasenotes/notes/oslo-policy-e142fa9243a8dcf6.yaml deleted file mode 100644 index ab3d57bb..00000000 --- a/releasenotes/notes/oslo-policy-e142fa9243a8dcf6.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -features: - - | - Use of oslo mechanisms for defining and enforcing policy. - A command line entry point that allows the user to generate a sample policy file. diff --git a/releasenotes/notes/osloconfiggen-d8a0f0a8d1acb961.yaml b/releasenotes/notes/osloconfiggen-d8a0f0a8d1acb961.yaml deleted file mode 100644 index 8415aef8..00000000 --- a/releasenotes/notes/osloconfiggen-d8a0f0a8d1acb961.yaml +++ /dev/null @@ -1,18 +0,0 @@ ---- -prelude: > - Matching OpenStack projects in configuration handling. -other: - - Removed configuration file from project tree. It is now generated via - oslo-config-generator utility. - - Moved all the code where configuration options are registered into single - location to mimic the configuration file (i.e. provide single place as it - was with configuration file). - - Removed final place where configuration is duplicated. In other words - person providing and/or modyfying options could have been forced to include - them also in the configuration file held in the tree. Not the only place - where it is required is Python codebase. - - Devstack plugin is also using new utility to provide configuration for - monasca-log-api launched inside the VM or Gate environment. diff --git a/releasenotes/notes/project_tree-3a041cbffc83595a.yaml b/releasenotes/notes/project_tree-3a041cbffc83595a.yaml deleted file mode 100644 index 3d3b852a..00000000 --- a/releasenotes/notes/project_tree-3a041cbffc83595a.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -other: - - | - Refactored project tree into more predictable and organized - one. Old tree had everythin mixed up between different locations, - and it was very difficult to follow the code. diff --git a/releasenotes/notes/use-standard-config-path-f47c856009b6410d.yaml b/releasenotes/notes/use-standard-config-path-f47c856009b6410d.yaml deleted file mode 100644 index d5028a5c..00000000 --- a/releasenotes/notes/use-standard-config-path-f47c856009b6410d.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -deprecations: - - | - Configuration file path /etc/monasca/log-api.conf is deprecated. - Use the standard path /etc/monasca/monasca-log-api.conf or the - configuration dir (supported via oslo.config) - /etc/monasca/monasca-log-api.conf.d/any_config_name.conf diff --git a/releasenotes/notes/uwsgi-0bf04f0ecd9c7522.yaml b/releasenotes/notes/uwsgi-0bf04f0ecd9c7522.yaml deleted file mode 100644 index 0b7c14dd..00000000 --- a/releasenotes/notes/uwsgi-0bf04f0ecd9c7522.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -prelude: > - https://governance.openstack.org/tc/goals/pike/deploy-api-in-wsgi.html -features: - - | - According to Pike release goals, all APIs should have the possibility - to be deployed under WSGI. With this change, monasca-log-api is capable - of running on either of following: gunicorn, uwsgi or mod_wsgi. diff --git a/releasenotes/notes/zuul_v3-b4138cd73cb6117a.yaml b/releasenotes/notes/zuul_v3-b4138cd73cb6117a.yaml deleted file mode 100644 index ead2d83b..00000000 --- a/releasenotes/notes/zuul_v3-b4138cd73cb6117a.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -upgrade: - - Upgrade integrations tests setup for Zuul V3. diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py deleted file mode 100644 index f4166433..00000000 --- a/releasenotes/source/conf.py +++ /dev/null @@ -1,252 +0,0 @@ -# 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. - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.6' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'openstackdocstheme', - 'reno.sphinxext' -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -openstackdocs_repo_name = u'openstack/monasca-log-api' -openstackdocs_auto_name = False -project = u'Monasca Log Release Notes' -openstackdocs_bug_project = u'monasca-log-api' -openstackdocs_bug_tag = u'releasenotes' -copyright = u'2014, OpenStack Foundation' - -# Release notes do not need a version number in the title, they -# cover multiple releases. -version = '' -release = '' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'native' - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'openstackdocs' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = [] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# html_extra_path = [] - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'MonascaLogApiReleaseNotesdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # 'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'MonascaLogApiReleaseNotes.tex', - u'MonascaLogApi Release Notes Documentation', u'OpenStack Foundation', - 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'barbicanreleasenotes', u'MonascaLogApi Release Notes Documentation', - [u'OpenStack Foundation'], 1) -] - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'MonascaLogApiReleaseNotes', u'MonascaLogApi Release Notes Documentation', - u'OpenStack Foundation', 'MonascaLogApiReleaseNotes', - 'MonascaLogApi Release Notes Documentation.', 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False - -# -- Options for Internationalization output ------------------------------ -locale_dirs = ['locale/'] diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst deleted file mode 100644 index c0c0fa72..00000000 --- a/releasenotes/source/index.rst +++ /dev/null @@ -1,15 +0,0 @@ -=========================== -MonascaLogApi Release Notes -=========================== - -Contents: - -.. toctree:: - :maxdepth: 1 - - unreleased - train - stein - rocky - queens - pike diff --git a/releasenotes/source/locale/.gitkeep b/releasenotes/source/locale/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst deleted file mode 100644 index e43bfc0c..00000000 --- a/releasenotes/source/pike.rst +++ /dev/null @@ -1,6 +0,0 @@ -=================================== - Pike Series Release Notes -=================================== - -.. release-notes:: - :branch: stable/pike diff --git a/releasenotes/source/queens.rst b/releasenotes/source/queens.rst deleted file mode 100644 index 36ac6160..00000000 --- a/releasenotes/source/queens.rst +++ /dev/null @@ -1,6 +0,0 @@ -=================================== - Queens Series Release Notes -=================================== - -.. release-notes:: - :branch: stable/queens diff --git a/releasenotes/source/rocky.rst b/releasenotes/source/rocky.rst deleted file mode 100644 index 40dd517b..00000000 --- a/releasenotes/source/rocky.rst +++ /dev/null @@ -1,6 +0,0 @@ -=================================== - Rocky Series Release Notes -=================================== - -.. release-notes:: - :branch: stable/rocky diff --git a/releasenotes/source/stein.rst b/releasenotes/source/stein.rst deleted file mode 100644 index efaceb66..00000000 --- a/releasenotes/source/stein.rst +++ /dev/null @@ -1,6 +0,0 @@ -=================================== - Stein Series Release Notes -=================================== - -.. release-notes:: - :branch: stable/stein diff --git a/releasenotes/source/train.rst b/releasenotes/source/train.rst deleted file mode 100644 index 58390039..00000000 --- a/releasenotes/source/train.rst +++ /dev/null @@ -1,6 +0,0 @@ -========================== -Train Series Release Notes -========================== - -.. release-notes:: - :branch: stable/train diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst deleted file mode 100644 index cd22aabc..00000000 --- a/releasenotes/source/unreleased.rst +++ /dev/null @@ -1,5 +0,0 @@ -============================== - Current Series Release Notes -============================== - -.. release-notes:: diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0dd25c3d..00000000 --- a/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. - -pbr!=2.1.0,>=2.0.0 # Apache-2.0 -Paste>=2.0.2 # MIT -falcon>=2.0.0 # Apache-2.0 -keystonemiddleware>=4.17.0 # Apache-2.0 -oslo.config>=5.2.0 # Apache-2.0 -oslo.context>=2.19.2 # Apache-2.0 -oslo.middleware>=3.31.0 # Apache-2.0 -oslo.log>=3.36.0 # Apache-2.0 -oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 -oslo.utils>=3.33.0 # Apache-2.0 -PasteDeploy>=1.5.0 # MIT -monasca-common>=2.7.0 # Apache-2.0 -eventlet!=0.18.3,!=0.20.1,!=0.21.0,>=0.18.2 # MIT -monasca-statsd>=1.1.0 # Apache-2.0 -simplejson>=3.8.1 # MIT diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 9cc9b567..00000000 --- a/setup.cfg +++ /dev/null @@ -1,59 +0,0 @@ -[metadata] -name = monasca-log-api -summary = Monasca API for sending log entries -description-file = - README.rst -author = OpenStack -author-email = openstack-discuss@lists.openstack.org -home-page = https://docs.openstack.org/monasca-log-api/latest/ -python-requires = >=3.6 -classifier = - Environment :: OpenStack - Intended Audience :: Information Technology - Intended Audience :: System Administrators - License :: OSI Approved :: Apache Software License - Operating System :: POSIX :: Linux - Programming Language :: Python - Programming Language :: Python :: Implementation :: CPython - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - -[files] -packages = - monasca_log_api - -data_files = - etc/monasca = - etc/monasca/log-api-paste.ini - etc/monasca/log-api-logging.conf - -[entry_points] -console_scripts = - monasca-log-api = monasca_log_api.app.main:main - -wsgi_scripts = - monasca-log-api-wsgi = monasca_log_api.app.wsgi:main - -oslo.config.opts = - monasca_log_api = monasca_log_api.conf:list_opts - -oslo.policy.policies = - monasca_log_api = monasca_log_api.policies:list_rules - -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 - -[pbr] -autodoc_index_modules = True -autodoc_exclude_modules = - monasca_log_api.app.wsgi* - # NOTE(trebskit) we cannot document that because of side-effect - # of trying to load entire application and inability to find - # config files. - monasca_log_api.tests.* - monasca_log_api_tempest.* -api_doc_dir = contributor/api diff --git a/setup.py b/setup.py deleted file mode 100644 index cd35c3c3..00000000 --- a/setup.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# -# 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 setuptools - -setuptools.setup( - setup_requires=['pbr>=2.0.0'], - pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index db747947..00000000 --- a/test-requirements.txt +++ /dev/null @@ -1,21 +0,0 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. - -# Install bounded pep8/pyflakes first, then let flake8 install -hacking>=3.0,<3.1.0 # Apache-2.0 -bandit!=1.6.0,>=1.1.0 # Apache-2.0 -bashate>=0.5.1 # Apache-2.0 - -fixtures>=3.0.0 # Apache-2.0/BSD -coverage!=4.4,>=4.0 # Apache-2.0 -oslotest>=3.2.0 # Apache-2.0 -stestr>=1.0.0 # Apache-2.0 -simplejson>=3.8.1 # MIT - -# documentation -doc8>=0.6.0 # Apache-2.0 -sphinx>=2.0.0,!=2.1.0 # BSD -os-api-ref>=1.5.0 # Apache-2.0 -reno>=3.1.0 # Apache-2.0 -openstackdocstheme>=2.2.1 # Apache-2.0 diff --git a/tools/bashate.sh b/tools/bashate.sh deleted file mode 100644 index 33d5bebb..00000000 --- a/tools/bashate.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -# Ignore too long lines error E006 from bashate and treat -# E005, E042 as errors. -SH_FILES=$(find $1 -type d -name files -prune -o -type f -name '*.sh' -print) -bashate -v -iE006 -eE005,E042 ${SH_FILES:-''} diff --git a/tox.ini b/tox.ini deleted file mode 100644 index cc43ce7f..00000000 --- a/tox.ini +++ /dev/null @@ -1,158 +0,0 @@ -[tox] -envlist = py37,pep8,cover -minversion = 3.1.1 -ignore_basepython_conflict = True -skipsdist = True - -[testenv] -basepython = python3 -usedevelop = True -setenv = VIRTUAL_ENV={envdir} - OS_TEST_PATH=monasca_log_api/tests -passenv = *_proxy - *_PROXY -whitelist_externals = bash - find - rm -install_command = pip install {opts} {packages} -deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt -commands = - find ./ -type f -name '*.pyc' -delete - stestr run {posargs} - -[testenv:cover] -description = Calculates code coverage -setenv = - PYTHON=coverage run --source monasca_log_api --parallel-mode -commands = - stestr run {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - -[testenv:debug] -description = Allows to run unit-test with debug mode enabled -commands = - oslo_debug_helper -t {toxinidir}/monasca_log_api/tests {posargs} - -[testenv:bashate] -description = Validates (pep8-like) devstack plugins -skip_install = True -usedevelop = False -commands = bash {toxinidir}/tools/bashate.sh {toxinidir}/devstack - -[testenv:bandit] -description = Validates codebase with bandit -skip_install = True -usedevelop = False -commands = - # FIXME(dmllr); B101 needs to be fixed first - bandit -r monasca_log_api -n5 -s B101 -x monasca_log_api/tests - -[testenv:pep8] -description = Runs set of linters against codebase (flake8, bandit, bashate, checkniceness) -commands = - {[testenv:flake8]commands} - {[testenv:bandit]commands} - {[testenv:bashate]commands} - {[testenv:checkniceness]commands} - -[testenv:flake8] -description = Validates codebase with flake -commands = - flake8 monasca_log_api - -[testenv:genconfig] -description = Generates sample documentation file for monasca-log-api -commands = oslo-config-generator --config-file=config-generator/monasca-log-api.conf - -[testenv:genpolicy] -description = Generates sample policy.json file for monasca-log-api -commands = oslopolicy-sample-generator --config-file=config-generator/policy.conf - -[testenv:docs] -description = Builds main documention -commands = - {[testenv:devdocs]commands} - -[testenv:api-guide] -description = Called from CI scripts to test and publish the API Guide -commands = - rm -rf api-guide/build - {[testenv:checkjson]commands} - sphinx-build -W -b html -d api-guide/build/doctrees api-guide/source api-guide/build/html - -[testenv:api-ref] -description = Called from CI scripts to test and publish the API Ref -commands = - rm -rf api-ref/build - {[testenv:checkjson]commands} - sphinx-build -W -b html -d api-ref/build/doctrees api-ref/source api-ref/build/html - -[testenv:releasenotes] -description = Called from CI script to test and publish the Release Notes -commands = - rm -rf releasenotes/build - sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html - -[testenv:all-docs] -description = Builds all docouments -commands = - {[testenv:devdocs]commands} - {[testenv:api-guide]commands} - {[testenv:api-ref]commands} - {[testenv:releasenotes]commands} - -[testenv:devdocs] -description = Builds developer documentation -commands = - rm -rf doc/build - rm -rf doc/source/contributor/api - {[testenv:checkjson]commands} - sphinx-build -a -E -d doc/build/doctrees -b html doc/source doc/build/html - -[testenv:checkniceness] -description = Validates (pep-like) documenation -skip_install = True -usedevelop = False -commands = - doc8 --file-encoding utf-8 {toxinidir}/doc - doc8 --file-encoding utf-8 {toxinidir}/api-ref - doc8 --file-encoding utf-8 {toxinidir}/api-guide - doc8 --file-encoding utf-8 {toxinidir}/releasenotes - -[testenv:checkjson] -description = Validates all json samples inside doc folder -skip_install = True -usedevelop = False -deps = -whitelist_externals = - bash - python - find - rm -commands = - bash -c "! find doc/ -type f -name *.json | xargs grep -U -n $'\r'" - bash -c '! find doc/ -type f -name *.json | xargs -t -n1 python -m json.tool 2>&1 > /dev/null | grep -B1 -v ^python' - -[testenv:venv] -commands = {posargs} - -[flake8] -max-line-length = 100 -exclude = .git,.tox,dist,doc,api-ref,api-guide,releasenotes,documentation,*.egg,build -show-source = True -enable-extensions = H203,H106 - -[hacking] -import_exceptions = - six.moves - -[testenv:lower-constraints] -deps = - -c{toxinidir}/lower-constraints.txt - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt