├── VERSION ├── misc ├── .gitignore ├── fluentd │ ├── Gemfile │ └── Dockerfile ├── docker-compose-run-efk.yml ├── efk-conf │ └── fluentd │ │ └── conf │ │ └── fluent.conf └── Makefile ├── Gemfile ├── .gitignore ├── test ├── helper.rb └── plugin │ └── test_out_swift.rb ├── Rakefile ├── Makefile ├── docker-compose.yml ├── .travis.yml ├── Dockerfile ├── fluent-plugin-swift.gemspec ├── README.rdoc ├── LICENSE.md └── lib └── fluent └── plugin └── out_swift.rb /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.4rc2 2 | -------------------------------------------------------------------------------- /misc/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ~* 2 | #* 3 | *~ 4 | [._]*.s[a-w][a-z] 5 | .DS_Store 6 | 7 | *.gem 8 | .bundle 9 | Gemfile.lock 10 | vendor 11 | .ruby-version 12 | 13 | test/tmp/ 14 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.expand_path("../../", __FILE__)) 2 | require "test-unit" 3 | require "fluent/test" 4 | require "fluent/test/driver/output" 5 | require "fluent/test/helpers" 6 | 7 | Test::Unit::TestCase.include(Fluent::Test::Helpers) 8 | Test::Unit::TestCase.extend(Fluent::Test::Helpers) 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rake/testtask' 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs.push("lib", "test") 8 | t.test_files = FileList["test/**/test_*.rb"] 9 | t.verbose = true 10 | t.warning = false 11 | end 12 | 13 | task default: [:test] 14 | -------------------------------------------------------------------------------- /misc/fluentd/Gemfile: -------------------------------------------------------------------------------- 1 | #source 'https://rubygems.org' 2 | 3 | gem 'fluent-plugin-elasticsearch', '~>2.8.5' 4 | gem 'fluent-plugin-rewrite-tag-filter' 5 | gem 'fluent-plugin-grep' 6 | gem 'fluent-plugin-parser' 7 | gem 'fluent-plugin-grok-parser' 8 | gem 'fluent-plugin-detect-exceptions', '~>0.0.9' 9 | gem 'fluent-plugin-multi-format-parser', '~>1.0.0' 10 | #gem 'fluent-plugin-systemd', '~>0.3.1' 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | name = fluent-plugin-swift 2 | build: 3 | docker-compose build --no-cache --force-rm 4 | up: 5 | docker-compose up -d 6 | down: 7 | docker-compose down 8 | copy-gem: up 9 | id=$$(docker-compose ps -q $(name)) ; \ 10 | version=$(shell cat VERSION) ; \ 11 | docker-compose exec -T $(name) ls -l /app/pkg/$(name)-$$version.gem ; \ 12 | docker cp $$id:/app/pkg/$(name)-$$version.gem . ; \ 13 | docker cp $$id:/app/pkg/$(name)-$$version.gem misc/fluentd/ 14 | docker-compose down 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | fluent-plugin-swift: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | args: 8 | - http_proxy=$http_proxy 9 | - https_proxy=$https_proxy 10 | - no_proxy=$no_proxy 11 | - RUBY_URL 12 | - MIRROR_DEBIAN 13 | environment: 14 | - http_proxy=$http_proxy 15 | - https_proxy=$https_proxy 16 | - no_proxy=$no_proxy 17 | - RUBY_URL 18 | - MIRROR_DEBIAN 19 | entrypoint: tail -f /etc/hosts 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | 4 | rvm: 5 | - 2.1.10 6 | - 2.2.10 7 | - 2.3.8 8 | - 2.4.5 9 | - 2.5.3 10 | - ruby-head 11 | 12 | gemfile: 13 | - Gemfile 14 | 15 | #branches: 16 | # only: 17 | # - master 18 | # - v0.12 19 | 20 | before_install: 21 | - gem update --system 22 | - gem update bundler 23 | script: 24 | - bundle exec rake test 25 | 26 | matrix: 27 | allow_failures: 28 | - rvm: ruby-head 29 | - rvm: 2.5.3 30 | - rvm: 2.4.5 31 | 32 | addons: 33 | apt: 34 | packages: 35 | - net-tools 36 | - zlib1g-dev 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | ARG RUBY_URL 3 | ARG MIRROR_DEBIAN 4 | ENV app /app 5 | RUN mkdir $app 6 | ADD . $app 7 | WORKDIR $app 8 | RUN buildDeps="sudo make gcc g++ libc-dev ruby-dev build-essential git zlib1g-dev liblzma-dev net-tools" && \ 9 | echo "$http_proxy $no_proxy" && set -x && [ -z "$MIRROR_DEBIAN" ] || \ 10 | sed -i.orig -e "s|http://deb.debian.org\([^[:space:]]*\)|$MIRROR_DEBIAN/debian9|g ; s|http://security.debian.org\([^[:space:]]*\)|$MIRROR_DEBIAN/debian9-security|g" /etc/apt/sources.list ; \ 11 | apt-get update -qq && \ 12 | apt-get install -qy --no-install-recommends $buildDeps && \ 13 | ( set -ex ; echo 'gem: --no-document' >> /etc/gemrc && \ 14 | [ -z "$http_proxy" ] || gem_args=" $gem_args -r -p $http_proxy " ; \ 15 | [ -z "$RUBY_URL" ] || sudo -E gem source -r https://rubygems.org/ ; \ 16 | [ -z "$RUBY_URL" ] || sudo -E gem source -a $RUBY_URL ; \ 17 | [ -z "$RUBY_URL" ] || sudo -E gem source -c ; \ 18 | sudo -E gem sources ; \ 19 | sudo -E gem install -V --no-rdoc --no-ri $gem_args bundler ) && \ 20 | [ -z "$RUBY_URL" ] || bundle config mirror.https://rubygems.org $RUBY_URL ; \ 21 | bundler install && \ 22 | bundle exec rake test && \ 23 | bundle exec rake build 24 | -------------------------------------------------------------------------------- /misc/docker-compose-run-efk.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | networks: 3 | efknetwork: 4 | driver: bridge 5 | driver_opts: 6 | com.docker.network.driver.mtu: 1450 7 | services: 8 | cloud: 9 | image: openstack-swift-keystone-docker 10 | restart: always 11 | networks: 12 | efknetwork: 13 | aliases: 14 | - cloud 15 | ports: 16 | - 35000:5000 17 | - 35357:35357 18 | - 38080:8080 19 | 20 | fluentd: 21 | image: "${fluentd_image_full}" 22 | build: 23 | context: fluentd 24 | dockerfile: Dockerfile 25 | args: 26 | - http_proxy=$http_proxy 27 | - https_proxy=$https_proxy 28 | - no_proxy=$no_proxy 29 | - RUBY_URL 30 | - MIRROR_DEBIAN 31 | - PLUGIN_VERSION 32 | environment: 33 | - OS_AUTH_URL 34 | - OS_USERNAME 35 | - OS_PASSWORD 36 | - OS_PROJECT_NAME 37 | - OS_PROJECT_DOMAIN_NAME 38 | - OS_REGION_NAME 39 | restart: always 40 | networks: 41 | efknetwork: 42 | aliases: 43 | - fluentd 44 | ports: 45 | - 24224 46 | - 5140 47 | - 10514/udp 48 | volumes: 49 | - "${efk_stack_conf_dir}/fluentd/conf:/fluentd/etc:rw" 50 | - "${efk_stack_data_dir}/logs:/fluentd/log" 51 | # entrypoint: tail -f /etc/hosts 52 | -------------------------------------------------------------------------------- /fluent-plugin-swift.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | $:.push File.expand_path('../lib', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.name = "fluent-plugin-swift" 6 | gem.description = "OpenStack Storage Service (Swift) output plugin for Fluentd event collector" 7 | gem.homepage = "https://github.com/yuuzi41/fluent-plugin-swift" 8 | gem.summary = gem.description 9 | gem.version = File.read("VERSION").strip 10 | gem.license = "Apache-2.0" 11 | gem.authors = ["yuuzi41"] 12 | gem.email = "" 13 | #gem.has_rdoc = false 14 | #gem.platform = Gem::Platform::RUBY 15 | gem.files = `git ls-files`.split("\n") 16 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | gem.require_paths = ['lib'] 19 | 20 | gem.add_runtime_dependency "fluentd", [">= 0.14.2", "< 2"] 21 | gem.add_runtime_dependency "fog-openstack" 22 | gem.add_runtime_dependency "uuidtools" 23 | gem.add_development_dependency "flexmock", ">= 1.2.0" 24 | gem.add_development_dependency "bundler", "~> 1.14" 25 | gem.add_development_dependency "rake", "~> 12.0" 26 | gem.add_development_dependency "test-unit", ">= 3.1.0" 27 | # fog 28 | gem.add_dependency("xmlrpc") if RUBY_VERSION.to_s >= "2.4" 29 | end 30 | -------------------------------------------------------------------------------- /misc/efk-conf/fluentd/conf/fluent.conf: -------------------------------------------------------------------------------- 1 | 2 | log_level info 3 | 4 | 5 | @type forward 6 | 7 | 8 | # Use the forward Input plugin and the fluent-cat command to feed events: 9 | # $ echo '{"event":"message"}' | fluent-cat test.tag 10 | 11 | @type copy 12 | 13 | # Dump the matched events. 14 | 15 | @type stdout 16 | 17 | 18 | # Feed the dumped events to your plugin. 19 | 20 | @type swift 21 | auth_url "#{ENV['OS_AUTH_URL']}" 22 | project_name "#{ENV['OS_PROJECT_NAME']}" 23 | auth_user "#{ENV['OS_USERNAME']}" 24 | auth_api_key "#{ENV['OS_PASSWORD']}" 25 | domain_name "#{ENV['OS_PROJECT_DOMAIN_NAME']}" 26 | auth_region "#{ENV['OS_REGION_NAME']}" 27 | ssl_verify false 28 | # auth_url http://cloud:5000/v3 29 | # project_name test 30 | # auth_user demo 31 | # auth_api_key demo 32 | # domain_name "Default" 33 | # auth_region RegionOne 34 | 35 | swift_container CONTAINER_NAME 36 | path logs/${tag}/%Y/%m/%d/ 37 | swift_object_key_format %{path}%{time_slice}_%{index}.%{file_extension} 38 | # swift_object_key_format %{path}%{uuid_flush}-%{time_slice}_%{index}.%{file_extension} 39 | # swift_object_key_format %{path}%{uuid}-%{time_slice}_%{index}.%{file_extension} 40 | 41 | 42 | @type file 43 | path /fluentd/log/app/swift 44 | timekey 30s 45 | timekey_wait 5s 46 | timekey_use_utc true 47 | 48 | 49 | @type json 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /misc/fluentd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fluent/fluentd:v1.1.3-debian 2 | ARG RUBY_URL 3 | ARG MIRROR_DEBIAN 4 | ARG PLUGIN_VERSION 5 | COPY Gemfile /Gemfile 6 | COPY fluent-plugin-swift-$PLUGIN_VERSION.gem / 7 | RUN buildDeps="sudo make gcc g++ libc-dev ruby-dev build-essential zlib1g-dev liblzma-dev" ; \ 8 | runDeps="net-tools" ; \ 9 | echo "$http_proxy $no_proxy" && set -x && [ -z "$MIRROR_DEBIAN" ] || \ 10 | sed -i.orig -e "s|http://deb.debian.org\([^[:space:]]*\)|$MIRROR_DEBIAN/debian9|g ; s|http://security.debian.org\([^[:space:]]*\)|$MIRROR_DEBIAN/debian9-security|g" /etc/apt/sources.list ; \ 11 | apt-get update -qq && \ 12 | apt-get install -y --no-install-recommends $buildDeps $runDeps && \ 13 | ( set -ex ; echo 'gem: --no-document' >> /etc/gemrc && \ 14 | [ -z "$http_proxy" ] || gem_proxy=" -p $http_proxy " ; \ 15 | [ -z "$http_proxy" ] || gem_args=" $gem_args -r $gem_proxy " ; \ 16 | [ -z "$RUBY_URL" ] || sudo -E gem source -r https://rubygems.org/ ; \ 17 | [ -z "$RUBY_URL" ] || sudo -E gem source -a $RUBY_URL ; \ 18 | [ -z "$RUBY_URL" ] || sudo -E gem source -c ; \ 19 | sudo -E gem sources ; \ 20 | cp /fluent-plugin-swift-$PLUGIN_VERSION.gem /var/lib/gems/2.3.0/cache/ ; \ 21 | sudo -E gem install -V --file /Gemfile --no-rdoc --no-ri $gem_args ; \ 22 | sudo -E gem install -V --no-rdoc --no-ri $gem_proxy /fluent-plugin-swift-$PLUGIN_VERSION.gem ; \ 23 | ) \ 24 | && sudo -E gem sources --clear-all \ 25 | && SUDO_FORCE_REMOVE=yes \ 26 | apt-get purge -y --auto-remove \ 27 | -o APT::AutoRemove::RecommendsImportant=false \ 28 | $buildDeps \ 29 | && rm -rf /var/lib/apt/lists/* \ 30 | /home/fluent/.gem/ruby/2.3.0/cache/*.gem 31 | 32 | -------------------------------------------------------------------------------- /misc/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # definition variables de environnement de test 3 | # 4 | export EFK_SERVICE_NAME = efk 5 | export APP = debug 6 | export APP_PATH := $(shell pwd) 7 | export APP_DATA := $(APP_PATH)/data 8 | 9 | export COMPOSE_PROJECT_NAME = test_${APP}_${EFK_SERVICE_NAME} 10 | 11 | export TEST_APP_PATH=${APP_PATH} 12 | export TEST_APP_DATA=${APP_DATA} 13 | 14 | export EFK_DOCKER_COMPOSE_RUN = ${TEST_APP_PATH}/docker-compose-run-${EFK_SERVICE_NAME}.yml 15 | 16 | export efk_stack_conf_dir = ${TEST_APP_PATH}/${EFK_SERVICE_NAME}-conf 17 | export efk_stack_data_dir = ${TEST_APP_DATA} 18 | 19 | export fluentd_image_full = ${APP}-fluentd:v1.1.3-debian 20 | export PLUGIN_VERSION=$(shell cat ../VERSION) 21 | build: 22 | docker-compose -f ${EFK_DOCKER_COMPOSE_RUN} build --no-cache --force-rm 23 | 24 | up: up-cloud up-fluentd run-test 25 | 26 | run-test: 27 | id=$$(docker-compose -f ${EFK_DOCKER_COMPOSE_RUN} up -d fluentd) ; \ 28 | docker-compose -f ${EFK_DOCKER_COMPOSE_RUN} exec -T fluentd /bin/bash -c 'for i in $$(seq 1 10) ; do echo "{\"event\":\"message$$i\"}" | fluent-cat test.tag ; done' 29 | 30 | up-fluentd: 31 | docker-compose -f ${EFK_DOCKER_COMPOSE_RUN} up fluentd 32 | up-cloud: 33 | docker-compose -f ${EFK_DOCKER_COMPOSE_RUN} up -d cloud 34 | docker-compose -f ${EFK_DOCKER_COMPOSE_RUN} exec cloud /bin/bash -c 'timeout=120 ; ret=1 ; until [ "$$timeout" -le 0 -o "$$ret" -eq "0" ] ; do curl -s --fail -XGET http://127.0.0.1:35357/v3 ; ret=$$? ; echo "Wait $$timeout" ; ((timeout--)) ; sleep 1 ; done' 35 | docker-compose -f ${EFK_DOCKER_COMPOSE_RUN} exec cloud /bin/bash -c '/swift/bin/register-swift-endpoint.sh http://cloud:8080' 36 | 37 | down: 38 | docker-compose -f ${EFK_DOCKER_COMPOSE_RUN} down 39 | down-fluentd: 40 | docker-compose -f ${EFK_DOCKER_COMPOSE_RUN} stop fluentd 41 | docker-compose -f ${EFK_DOCKER_COMPOSE_RUN} rm -f fluentd 42 | -------------------------------------------------------------------------------- /test/plugin/test_out_swift.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | require "fluent/plugin/out_swift" 3 | 4 | class SwiftOutputTest < Test::Unit::TestCase 5 | setup do 6 | Fluent::Test.setup 7 | end 8 | 9 | def teardown 10 | Dir.glob('test/tmp/*').each {|file| FileUtils.rm_f(file) } 11 | end 12 | 13 | CONFIG_NONE = %[ 14 | swift_container CONTAINER_NAME 15 | ] 16 | 17 | CONFIG_ENV = %[ 18 | swift_container CONTAINER_NAME 19 | auth_url "#{ENV['EMPTY_OS_AUTH_URL']}" 20 | auth_user test:tester 21 | auth_api_key testing 22 | ] 23 | 24 | CONFIG = %[ 25 | auth_url https://127.0.0.1/auth/v3 26 | auth_user test:tester 27 | auth_api_key testing 28 | domain_name default 29 | project_name test_project 30 | auth_region RegionOne 31 | swift_container CONTAINER_NAME 32 | path logs/ 33 | swift_object_key_format %{path}%{time_slice}_%{index}.%{file_extension} 34 | ssl_verify false 35 | buffer_path /var/log/fluent/swift 36 | buffer_type memory 37 | time_slice_format %Y%m%d-%H 38 | time_slice_wait 10m 39 | utc 40 | ] 41 | 42 | def create_driver(conf = CONFIG) 43 | Fluent::Test::Driver::Output.new(Fluent::Plugin::SwiftOutput) do 44 | def format(tag, time, record) 45 | super 46 | end 47 | 48 | def write(chunk) 49 | chunk.read 50 | end 51 | 52 | private 53 | 54 | def check_container 55 | end 56 | end.configure(conf) 57 | end 58 | 59 | def test_auth_url_absent 60 | assert_raise_message(/'auth_url' parameter is required/) do 61 | create_driver(CONFIG_NONE) 62 | end 63 | end 64 | 65 | def test_auth_url_empty 66 | assert_raise_message(/auth_url parameter or OS_AUTH_URL variable not defined/) do 67 | create_driver(CONFIG_ENV) 68 | end 69 | end 70 | 71 | def test_configure 72 | d = create_driver(CONFIG) 73 | assert_equal 'test:tester', d.instance.auth_user 74 | assert_equal 'testing', d.instance.auth_api_key 75 | assert_equal 'RegionOne', d.instance.auth_region 76 | assert_equal 'CONTAINER_NAME', d.instance.swift_container 77 | assert_equal 'logs/', d.instance.path 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = OpenStack Storage Service (swift) output plugin for Fluent event collector 2 | 3 | == Overview 4 | 5 | *swift* output plugin buffers event logs in local file and upload it to Swift periodically. 6 | 7 | This plugin splits files exactly by using the time of event logs (not the time when the logs are received). For example, a log '2011-01-02 message B' is reached, and then another log '2011-01-03 message B' is reached in this order, the former one is stored in "20110102.gz" file, and latter one in "20110103.gz" file. 8 | 9 | 10 | == Requirements 11 | 12 | | fluent-plugin-swift | fluentd | ruby | 13 | |-------------------|---------|------| 14 | | >= 0.0.4 | >= v0.14.0 | >= 2.1 | 15 | | < 0.0.4 | >= v0.12.0 | >= 1.9 | 16 | 17 | == Installation 18 | 19 | Simply use RubyGems: 20 | 21 | gem install fluent-plugin-swift 22 | 23 | == Configuration 24 | 25 | === v1.0 style 26 | 27 | With fluentd v1.0 and fluent-plugin-swift >=v0.0.4, use new buffer configuration to dynamic parameters. 28 | 29 | 30 | 31 | @type swift 32 | 33 | auth_url https://your.swift.proxy/auth/v1.0 _or_ https://your.keystone/v3.0 34 | auth_user test:tester 35 | auth_api_key testing 36 | project_name test-project 37 | domain_name "Default" 38 | auth_region RegionOne 39 | swift_container CONTAINER_NAME 40 | ssl_verify false 41 | 42 | path logs/ 43 | swift_object_key_format %{path}%{time_slice}_%{index}.%{file_extension} 44 | 45 | @type file 46 | path /var/log/fluent/swift 47 | timekey 3600 # 1 hour partition 48 | timekey_wait 10m 49 | timekey_use_utc true # use utc 50 | 51 | 52 | @type json 53 | 54 | 55 | 56 | 57 | Use OpenStack environment variables to configure parameters dynamically 58 | 59 | 60 | @type swift 61 | 62 | auth_url "#{ENV['OS_AUTH_URL']}" 63 | project_name "#{ENV['OS_PROJECT_NAME']}" 64 | auth_user "#{ENV['OS_USERNAME']}" 65 | auth_api_key "#{ENV['OS_PASSWORD']}" 66 | domain_name "#{ENV['OS_PROJECT_DOMAIN_NAME']}" 67 | auth_region "#{ENV['OS_REGION_NAME']}" 68 | swift_container CONTAINER_NAME 69 | ssl_verify false 70 | 71 | path logs/ 72 | swift_object_key_format %{path}%{time_slice}_%{index}.%{file_extension} 73 | 74 | @type file 75 | path /var/log/fluent/swift 76 | timekey 3600 # 1 hour partition 77 | timekey_wait 10m 78 | timekey_use_utc true # use utc 79 | 80 | 81 | @type json 82 | 83 | 84 | 85 | 86 | 87 | [auth_url] Authentication URL. If not set in conf, use env OS_AUTH_URL 88 | 89 | [auth_user] Authentication User Name. if you use TempAuth, auth_user is ACCOUNT:USER . If not set in conf, use env OS_USERNAME 90 | 91 | [auth_tenant (optional, for keystone v2)] Authentication Tenant. if you use TempAuth, this isn't required. 92 | 93 | [project_name (keystone v3)] Authentication Project. If not set in conf, use env OS_PROJECT_NAME 94 | [domain_name (keystone v3)] Authentication Domain. If not set in conf, use env OS_PROJECT_DOMAIN_NAME 95 | 96 | [auth_api_key] Authentication Key (Password). If not set in conf, use env OS_PASSWORD 97 | 98 | [auth_region] Authentication Region. Optional, not required if there is only one region available. If not set in conf, use env OS_REGION_NAME 99 | 100 | [swift_account (optional)] Account name. if this isn't provided, use default Account. 101 | 102 | [swift_container] Container name. 103 | 104 | [swift_object_key_format] The format of Swift object keys. You can use several built-in variables: 105 | 106 | - %{path} 107 | - %{time_slice} 108 | - %{index} 109 | - %{file_extension} 110 | 111 | to decide keys dynamically. 112 | 113 | %{path} is exactly the value of *path* configured in the configuration file. E.g., "logs/" in the example configuration above. 114 | %{time_slice} is the time-slice in text that are formatted with *time_slice_format*. 115 | %{index} is the sequential number starts from 0, increments when multiple files are uploaded to Swift in the same time slice. 116 | %{file_extention} is always "gz" for now. 117 | 118 | The default format is "%{path}%{time_slice}_%{index}.%{file_extension}". 119 | 120 | For instance, using the example configuration above, actual object keys on Swift will be something like: 121 | 122 | "logs/20130111-22_0.gz" 123 | "logs/20130111-23_0.gz" 124 | "logs/20130111-23_1.gz" 125 | "logs/20130112-00_0.gz" 126 | 127 | With the configuration: 128 | 129 | swift_object_key_format %{path}/events/ts=%{time_slice}/events_%{index}.%{file_extension} 130 | path log 131 | time_slice_format %Y%m%d-%H 132 | 133 | You get: 134 | 135 | "log/events/ts=20130111-22/events_0.gz" 136 | "log/events/ts=20130111-23/events_0.gz" 137 | "log/events/ts=20130111-23/events_1.gz" 138 | "log/events/ts=20130112-00/events_0.gz" 139 | 140 | The {fluent-mixin-config-placeholders}[https://github.com/tagomoris/fluent-mixin-config-placeholders] mixin is also incorporated, so additional variables such as %{hostname}, %{uuid}, etc. can be used in the swift_object_key_format. This could prove useful in preventing filename conflicts when writing from multiple servers. 141 | 142 | swift_object_key_format %{path}/events/ts=%{time_slice}/events_%{index}-%{hostname}.%{file_extension} 143 | 144 | [store_as] archive format on Swift. You can use serveral format: 145 | 146 | - gzip (default) 147 | - json 148 | - text 149 | - lzo (Need lzop command) 150 | 151 | [auto_create_container] Create Swift container if it does not exists. Default is true. 152 | 153 | [path] path prefix of the files on Swift. Default is "" (no prefix). 154 | 155 | [buffer_path (required)] path prefix of the files to buffer logs. 156 | 157 | [time_slice_format] Format of the time used as the file name. Default is '%Y%m%d'. Use '%Y%m%d%H' to split files hourly. 158 | 159 | [time_slice_wait] The time to wait old logs. Default is 10 minutes. Specify larger value if old logs may reache. 160 | 161 | [utc] Use UTC instead of local time. 162 | 163 | 164 | == Copyright 165 | 166 | Copyright:: Copyright (c) 2013 Yuji Hagiwara. 167 | 168 | This software is based on fluent-plugin-s3 ( https://github.com/fluent/fluent-plugin-s3 ), written by Sadayuki Furuhashi, licensed by Apache License, Version 2.0. 169 | 170 | License:: Apache License, Version 2.0 171 | 172 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | 193 | -------------------------------------------------------------------------------- /lib/fluent/plugin/out_swift.rb: -------------------------------------------------------------------------------- 1 | require 'fluent/plugin/output' 2 | require 'fluent/timezone' 3 | require 'fog/openstack' 4 | require 'zlib' 5 | require 'time' 6 | require 'tempfile' 7 | require 'open3' 8 | 9 | module Fluent::Plugin 10 | class SwiftOutput < Output 11 | Fluent::Plugin.register_output('swift', self) 12 | 13 | helpers :compat_parameters, :formatter, :inject 14 | 15 | def initialize 16 | super 17 | @uuid_flush_enabled = false 18 | end 19 | 20 | desc "Path prefix of the files on Swift" 21 | config_param :path, :string, :default => "" 22 | # openstack auth 23 | desc "Authentication URL. set a value or `#{ENV['OS_AUTH_URL']}`" 24 | config_param :auth_url, :string 25 | desc "Authentication User Name. if you use TempAuth, auth_user is ACCOUNT:USER .set a value or `#{ENV['OS_USERNAME']}`" 26 | config_param :auth_user, :string 27 | desc "Authentication Key (Password). set a value or `#{ENV['OS_PASSWORD']}`" 28 | config_param :auth_api_key, :string 29 | # identity v2 30 | config_param :auth_tenant, :string, default: nil 31 | # identity v3 32 | desc "Authentication Project. set a value or `#{ENV['OS_PROJECT_NAME']}`" 33 | config_param :project_name, :string, default: nil 34 | desc "Authentication Domain. set a value or `#{ENV['OS_PROJECT_DOMAIN_NAME']}`" 35 | config_param :domain_name, :string, default: nil 36 | desc "Authentication Region. Optional, not required if there is only one region available. set a value or `#{ENV['OS_REGION_NAME']}`" 37 | config_param :auth_region, :string, default: nil 38 | config_param :swift_account, :string, default: nil 39 | 40 | desc "Swift container name" 41 | config_param :swift_container, :string 42 | desc "Archive format on Swift" 43 | config_param :store_as, :string, :default => "gzip" 44 | desc "If false, the certificate of endpoint will not be verified" 45 | config_param :ssl_verify, :bool, :default => true 46 | desc "The format of Swift object keys" 47 | config_param :swift_object_key_format, :string, :default => "%{path}%{time_slice}_%{index}.%{file_extension}" 48 | desc "Create Swift container if it does not exists" 49 | config_param :auto_create_container, :bool, :default => true 50 | config_param :check_apikey_on_start, :bool, :default => true 51 | desc "URI of proxy environment" 52 | config_param :proxy_uri, :string, :default => nil 53 | desc "The length of `%{hex_random}` placeholder(4-16)" 54 | config_param :hex_random_length, :integer, default: 4 55 | desc "`sprintf` format for `%{index}`" 56 | config_param :index_format, :string, default: "%d" 57 | desc "Overwrite already existing path" 58 | config_param :overwrite, :bool, default: false 59 | 60 | DEFAULT_FORMAT_TYPE = "out_file" 61 | 62 | config_section :format do 63 | config_set_default :@type, DEFAULT_FORMAT_TYPE 64 | end 65 | 66 | config_section :buffer do 67 | config_set_default :chunk_keys, ['time'] 68 | config_set_default :timekey, (60 * 60 * 24) 69 | end 70 | 71 | # attr_reader :storage 72 | 73 | MAX_HEX_RANDOM_LENGTH = 16 74 | 75 | def configure(conf) 76 | compat_parameters_convert(conf, :buffer, :formatter, :inject) 77 | 78 | super 79 | 80 | if @auth_url.empty? 81 | raise Fluent::ConfigError, "auth_url parameter or OS_AUTH_URL variable not defined" 82 | end 83 | if @auth_user.empty? 84 | raise Fluent::ConfigError, "auth_user parameter or OS_USERNAME variable not defined" 85 | end 86 | if @auth_api_key.empty? 87 | raise Fluent::ConfigError, "auth_api_key parameter or OS_PASSWORD variable not defined" 88 | end 89 | 90 | if @project_name.empty? 91 | raise Fluent::ConfigError, "project_name parameter or OS_PROJECT_NAME variable not defined" 92 | end 93 | if @domain_name.empty? 94 | raise Fluent::ConfigError, "domain_name parameter or OS_PROJECT_DOMAIN_NAME variable not defined" 95 | end 96 | 97 | @ext, @mime_type = case @store_as 98 | when 'gzip' then ['gz', 'application/x-gzip'] 99 | when 'lzo' then 100 | begin 101 | Open3.capture3('lzop -V') 102 | rescue Errno::ENOENT 103 | raise ConfigError, "'lzop' utility must be in PATH for LZO compression" 104 | end 105 | ['lzo', 'application/x-lzop'] 106 | when 'json' then ['json', 'application/json'] 107 | else ['txt', 'text/plain'] 108 | end 109 | 110 | @formatter = formatter_create 111 | 112 | if @hex_random_length > MAX_HEX_RANDOM_LENGTH 113 | raise Fluent::ConfigError, "hex_random_length parameter must be less than or equal to #{MAX_HEX_RANDOM_LENGTH}" 114 | end 115 | 116 | unless @index_format =~ /^%(0\d*)?[dxX]$/ 117 | raise Fluent::ConfigError, "index_format parameter should follow `%[flags][width]type`. `0` is the only supported flag, and is mandatory if width is specified. `d`, `x` and `X` are supported types" 118 | end 119 | 120 | @swift_object_key_format = process_swift_object_key_format 121 | # For backward compatibility 122 | # TODO: Remove time_slice_format when end of support compat_parameters 123 | @configured_time_slice_format = conf['time_slice_format'] 124 | @values_for_swift_object_chunk = {} 125 | @time_slice_with_tz = Fluent::Timezone.formatter(@timekey_zone, @configured_time_slice_format || timekey_to_timeformat(@buffer_config['timekey'])) 126 | end 127 | 128 | def multi_workers_ready? 129 | true 130 | end 131 | 132 | def start 133 | 134 | Excon.defaults[:ssl_verify_peer] = @ssl_verify 135 | 136 | begin 137 | @storage = Fog::OpenStack::Storage.new(openstack_auth_url: @auth_url, 138 | openstack_username: @auth_user, 139 | openstack_project_name: @project_name, 140 | openstack_domain_name: @domain_name, 141 | openstack_api_key: @auth_api_key, 142 | openstack_region: @auth_region) 143 | # rescue Fog::OpenStack::Storage::NotFound 144 | # ignore NoSuchBucket Error because ensure_bucket checks it. 145 | rescue => e 146 | raise "can't call Swift API. Please check your ENV OS_*, your credentials or auth_url configuration. error = #{e.inspect}" 147 | end 148 | 149 | @storage.change_account @swift_account if @swift_account 150 | 151 | check_container 152 | 153 | super 154 | end 155 | 156 | def format(tag, time, record) 157 | r = inject_values_to_record(tag, time, record) 158 | @formatter.format(tag, time, r) 159 | end 160 | 161 | def write(chunk) 162 | i = 0 163 | metadata = chunk.metadata 164 | previous_path = nil 165 | time_slice = if metadata.timekey.nil? 166 | ''.freeze 167 | else 168 | @time_slice_with_tz.call(metadata.timekey) 169 | end 170 | 171 | begin 172 | @values_for_swift_object_chunk[chunk.unique_id] ||= { 173 | "%{hex_random}" => hex_random(chunk), 174 | } 175 | values_for_swift_object_key_pre = { 176 | "%{path}" => @path, 177 | "%{file_extension}" => @ext, 178 | } 179 | values_for_swift_object_key_post = { 180 | "%{time_slice}" => time_slice, 181 | "%{index}" => sprintf(@index_format,i), 182 | }.merge!(@values_for_swift_object_chunk[chunk.unique_id]) 183 | values_for_swift_object_key_post["%{uuid_flush}".freeze] = uuid_random if @uuid_flush_enabled 184 | 185 | swift_path = @swift_object_key_format.gsub(%r(%{[^}]+})) do |matched_key| 186 | values_for_swift_object_key_pre.fetch(matched_key, matched_key) 187 | end 188 | 189 | swift_path = extract_placeholders(swift_path, metadata) 190 | swift_path = swift_path.gsub(%r(%{[^}]+}), values_for_swift_object_key_post) 191 | if (i > 0) && (swift_path == previous_path) 192 | if @overwrite 193 | log.warn "#{swift_path} already exists, but will overwrite" 194 | break 195 | else 196 | raise "duplicated path is generated. use %{index} in swift_object_key_format: path = #{swift_path}" 197 | end 198 | end 199 | 200 | 201 | i += 1 202 | previous_path = swift_path 203 | end while check_object_exists(@swift_container, swift_path) 204 | 205 | 206 | tmp = Tempfile.new("swift-") 207 | tmp.binmode 208 | begin 209 | if @store_as == "gzip" 210 | w = Zlib::GzipWriter.new(tmp) 211 | chunk.write_to(w) 212 | w.close 213 | elsif @store_as == "lzo" 214 | w = Tempfile.new("chunk-tmp") 215 | chunk.write_to(w) 216 | w.close 217 | tmp.close 218 | # We don't check the return code because we can't recover lzop failure. 219 | system "lzop -qf1 -o #{tmp.path} #{w.path}" 220 | else 221 | chunk.write_to(tmp) 222 | tmp.close 223 | end 224 | File.open(tmp.path) do |file| 225 | @storage.put_object(@swift_container, swift_path, file, {:content_type => @mime_type}) 226 | @values_for_swift_object_chunk.delete(chunk.unique_id) 227 | end 228 | # log.debu "out_swift: write chunk #{dump_unique_id_hex(chunk.unique_id)} with metadata #{chunk.metadata} to swift://#{@swift_container}/#{swift_path}" 229 | # $log.info "out_swift: Put Log to Swift. container=#{@swift_container} object=#{swift_path}" 230 | ensure 231 | tmp.close(true) rescue nil 232 | w.close rescue nil 233 | w.unlink rescue nil 234 | end 235 | end 236 | 237 | private 238 | 239 | def hex_random(chunk) 240 | unique_hex = Fluent::UniqueId.hex(chunk.unique_id) 241 | unique_hex.reverse! # unique_hex is like (time_sec, time_usec, rand) => reversing gives more randomness 242 | unique_hex[0...@hex_random_length] 243 | end 244 | 245 | def uuid_random 246 | ::UUIDTools::UUID.random_create.to_s 247 | end 248 | 249 | # This is stolen from Fluentd 250 | def timekey_to_timeformat(timekey) 251 | case timekey 252 | when nil then '' 253 | when 0...60 then '%Y%m%d%H%M%S' # 60 exclusive 254 | when 60...3600 then '%Y%m%d%H%M' 255 | when 3600...86400 then '%Y%m%d%H' 256 | else '%Y%m%d' 257 | end 258 | end 259 | 260 | def check_container 261 | begin 262 | @storage.get_container(@swift_container) 263 | rescue Fog::OpenStack::Storage::NotFound 264 | if @auto_create_container 265 | $log.info "Creating container #{@swift_container} on #{@auth_url}, #{@swift_account}" 266 | @storage.put_container(@swift_container) 267 | else 268 | raise "The specified container does not exist: container = #{swift_container}" 269 | end 270 | end 271 | end 272 | 273 | def process_swift_object_key_format 274 | %W(%{uuid} %{uuid:random} %{uuid:hostname} %{uuid:timestamp}).each { |ph| 275 | if @swift_object_key_format.include?(ph) 276 | raise Fluent::ConfigError, %!#{ph} placeholder in swift_object_key_format is removed! 277 | end 278 | } 279 | 280 | if @swift_object_key_format.include?('%{uuid_flush}') 281 | # test uuidtools works or not 282 | begin 283 | require 'uuidtools' 284 | rescue LoadError 285 | raise Fluent::ConfigError, "uuidtools gem not found. Install uuidtools gem first" 286 | end 287 | begin 288 | uuid_random 289 | rescue => e 290 | raise Fluent::ConfigError, "Generating uuid doesn't work. Can't use %{uuid_flush} on this environment. #{e}" 291 | end 292 | @uuid_flush_enabled = true 293 | end 294 | 295 | @swift_object_key_format.gsub('%{hostname}') { |expr| 296 | log.warn "%{hostname} will be removed in the future. Use \"\#{Socket.gethostname}\" instead" 297 | Socket.gethostname 298 | } 299 | end 300 | 301 | def check_object_exists(container, object) 302 | begin 303 | @storage.head_object(container, object) 304 | rescue Fog::OpenStack::Storage::NotFound 305 | return false 306 | end 307 | return true 308 | end 309 | 310 | end 311 | end 312 | --------------------------------------------------------------------------------