├── 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 |
--------------------------------------------------------------------------------