fluent-plugin-kubernetes_metadata_filter 2.1.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +53 -0
  3. data/.gitignore +0 -2
  4. data/.rubocop.yml +57 -0
  5. data/Gemfile +4 -2
  6. data/Gemfile.lock +159 -0
  7. data/README.md +49 -60
  8. data/Rakefile +15 -11
  9. data/doc/benchmark/5m-1-2500lps-256b-baseline-01/cpu.png +0 -0
  10. data/doc/benchmark/5m-1-2500lps-256b-baseline-01/latency.png +0 -0
  11. data/doc/benchmark/5m-1-2500lps-256b-baseline-01/loss.png +0 -0
  12. data/doc/benchmark/5m-1-2500lps-256b-baseline-01/mem.png +0 -0
  13. data/doc/benchmark/5m-1-2500lps-256b-baseline-01/readme.md +88 -0
  14. data/doc/benchmark/5m-1-2500lps-256b-baseline-01/results.html +127 -0
  15. data/doc/benchmark/5m-1-2500lps-256b-kube-01-01/cpu.png +0 -0
  16. data/doc/benchmark/5m-1-2500lps-256b-kube-01-01/latency.png +0 -0
  17. data/doc/benchmark/5m-1-2500lps-256b-kube-01-01/loss.png +0 -0
  18. data/doc/benchmark/5m-1-2500lps-256b-kube-01-01/mem.png +0 -0
  19. data/doc/benchmark/5m-1-2500lps-256b-kube-01-01/readme.md +97 -0
  20. data/doc/benchmark/5m-1-2500lps-256b-kube-01-01/results.html +136 -0
  21. data/doc/benchmark/5m-1-2500lps-256b-kube-01-marshal-02/cpu.png +0 -0
  22. data/doc/benchmark/5m-1-2500lps-256b-kube-01-marshal-02/latency.png +0 -0
  23. data/doc/benchmark/5m-1-2500lps-256b-kube-01-marshal-02/loss.png +0 -0
  24. data/doc/benchmark/5m-1-2500lps-256b-kube-01-marshal-02/mem.png +0 -0
  25. data/doc/benchmark/5m-1-2500lps-256b-kube-01-marshal-02/readme.md +97 -0
  26. data/doc/benchmark/5m-1-2500lps-256b-kube-01-marshal-02/results.html +136 -0
  27. data/fluent-plugin-kubernetes_metadata_filter.gemspec +25 -28
  28. data/lib/fluent/plugin/filter_kubernetes_metadata.rb +207 -187
  29. data/lib/fluent/plugin/kubernetes_metadata_cache_strategy.rb +30 -23
  30. data/lib/fluent/plugin/kubernetes_metadata_common.rb +66 -24
  31. data/lib/fluent/plugin/kubernetes_metadata_stats.rb +21 -5
  32. data/lib/fluent/plugin/kubernetes_metadata_test_api_adapter.rb +68 -0
  33. data/lib/fluent/plugin/kubernetes_metadata_util.rb +33 -0
  34. data/lib/fluent/plugin/kubernetes_metadata_watch_namespaces.rb +154 -27
  35. data/lib/fluent/plugin/kubernetes_metadata_watch_pods.rb +171 -29
  36. data/release_notes.md +42 -0
  37. data/test/cassettes/kubernetes_docker_metadata_annotations.yml +0 -34
  38. data/test/cassettes/{kubernetes_docker_metadata_dotted_labels.yml → kubernetes_docker_metadata_dotted_slashed_labels.yml} +0 -34
  39. data/test/cassettes/kubernetes_get_api_v1.yml +193 -0
  40. data/test/cassettes/kubernetes_get_api_v1_using_token.yml +195 -0
  41. data/test/cassettes/kubernetes_get_namespace_default.yml +72 -0
  42. data/test/cassettes/kubernetes_get_namespace_default_using_token.yml +71 -0
  43. data/test/cassettes/{kubernetes_docker_metadata.yml → kubernetes_get_pod.yml} +0 -82
  44. data/test/cassettes/kubernetes_get_pod_container_init.yml +145 -0
  45. data/test/cassettes/{metadata_with_namespace_id.yml → kubernetes_get_pod_using_token.yml} +2 -130
  46. data/test/cassettes/{kubernetes_docker_metadata_using_bearer_token.yml → kubernetes_get_pod_with_ownerrefs.yml} +17 -109
  47. data/test/cassettes/metadata_from_tag_and_journald_fields.yml +153 -0
  48. data/test/cassettes/metadata_from_tag_journald_and_kubernetes_fields.yml +285 -0
  49. data/test/cassettes/{non_kubernetes_docker_metadata.yml → valid_kubernetes_api_server_using_token.yml} +4 -44
  50. data/test/helper.rb +20 -2
  51. data/test/plugin/test_cache_stats.rb +10 -13
  52. data/test/plugin/test_cache_strategy.rb +158 -160
  53. data/test/plugin/test_filter_kubernetes_metadata.rb +451 -314
  54. data/test/plugin/test_watch_namespaces.rb +209 -55
  55. data/test/plugin/test_watch_pods.rb +302 -71
  56. data/test/plugin/watch_test.rb +52 -33
  57. metadata +91 -70
  58. data/circle.yml +0 -17
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
3
5
  # Kubernetes metadata
@@ -16,46 +18,186 @@
16
18
  # See the License for the specific language governing permissions and
17
19
  # limitations under the License.
18
20
  #
21
+ # TODO: this is mostly copy-paste from kubernetes_metadata_watch_namespaces.rb unify them
19
22
  require_relative 'kubernetes_metadata_common'
20
23
 
21
24
  module KubernetesMetadata
22
25
  module WatchPods
23
-
24
26
  include ::KubernetesMetadata::Common
25
27
 
28
+ def set_up_pod_thread
29
+ # Any failures / exceptions in the initial setup should raise
30
+ # Fluent:ConfigError, so that users can inspect potential errors in
31
+ # the configuration.
32
+ pod_watcher = start_pod_watch
33
+
34
+ Thread.current[:pod_watch_retry_backoff_interval] = @watch_retry_interval
35
+ Thread.current[:pod_watch_retry_count] = 0
36
+
37
+ # Any failures / exceptions in the followup watcher notice
38
+ # processing will be swallowed and retried. These failures /
39
+ # exceptions could be caused by Kubernetes API being temporarily
40
+ # down. We assume the configuration is correct at this point.
41
+ loop do
42
+ pod_watcher ||= get_pods_and_start_watcher
43
+ process_pod_watcher_notices(pod_watcher)
44
+ rescue GoneError => e
45
+ # Expected error. Quietly go back through the loop in order to
46
+ # start watching from the latest resource versions
47
+ @stats.bump(:pod_watch_gone_errors)
48
+ log.info('410 Gone encountered. Restarting pod watch to reset resource versions.', e)
49
+ pod_watcher = nil
50
+ rescue KubeException => e
51
+ if e.error_code == 401
52
+ # recreate client to refresh token
53
+ log.info("Encountered '401 Unauthorized' exception in watch, recreating client to refresh token")
54
+ create_client()
55
+ pod_watcher = nil
56
+ else
57
+ # treat all other errors the same as StandardError, log, swallow and reset
58
+ @stats.bump(:pod_watch_failures)
59
+ if Thread.current[:pod_watch_retry_count] < @watch_retry_max_times
60
+ # Instead of raising exceptions and crashing Fluentd, swallow
61
+ # the exception and reset the watcher.
62
+ log.info(
63
+ 'Exception encountered parsing pod watch event. The ' \
64
+ 'connection might have been closed. Sleeping for ' \
65
+ "#{Thread.current[:pod_watch_retry_backoff_interval]} " \
66
+ 'seconds and resetting the pod watcher.', e
67
+ )
68
+ sleep(Thread.current[:pod_watch_retry_backoff_interval])
69
+ Thread.current[:pod_watch_retry_count] += 1
70
+ Thread.current[:pod_watch_retry_backoff_interval] *= @watch_retry_exponential_backoff_base
71
+ pod_watcher = nil
72
+ else
73
+ # Since retries failed for many times, log as errors instead
74
+ # of info and raise exceptions and trigger Fluentd to restart.
75
+ message =
76
+ 'Exception encountered parsing pod watch event. The ' \
77
+ 'connection might have been closed. Retried ' \
78
+ "#{@watch_retry_max_times} times yet still failing. Restarting."
79
+ log.error(message, e)
80
+ raise Fluent::UnrecoverableError, message
81
+ end
82
+ end
83
+ rescue StandardError => e
84
+ @stats.bump(:pod_watch_failures)
85
+ if Thread.current[:pod_watch_retry_count] < @watch_retry_max_times
86
+ # Instead of raising exceptions and crashing Fluentd, swallow
87
+ # the exception and reset the watcher.
88
+ log.info(
89
+ 'Exception encountered parsing pod watch event. The ' \
90
+ 'connection might have been closed. Sleeping for ' \
91
+ "#{Thread.current[:pod_watch_retry_backoff_interval]} " \
92
+ 'seconds and resetting the pod watcher.', e
93
+ )
94
+ sleep(Thread.current[:pod_watch_retry_backoff_interval])
95
+ Thread.current[:pod_watch_retry_count] += 1
96
+ Thread.current[:pod_watch_retry_backoff_interval] *= @watch_retry_exponential_backoff_base
97
+ pod_watcher = nil
98
+ else
99
+ # Since retries failed for many times, log as errors instead
100
+ # of info and raise exceptions and trigger Fluentd to restart.
101
+ message =
102
+ 'Exception encountered parsing pod watch event. The ' \
103
+ 'connection might have been closed. Retried ' \
104
+ "#{@watch_retry_max_times} times yet still failing. Restarting."
105
+ log.error(message, e)
106
+ raise Fluent::UnrecoverableError, message
107
+ end
108
+ end
109
+ end
110
+
26
111
  def start_pod_watch
27
- begin
28
- resource_version = @client.get_pods.resourceVersion
29
- watcher = @client.watch_pods(resource_version)
30
- rescue Exception => e
31
- message = "Exception encountered fetching metadata from Kubernetes API endpoint: #{e.message}"
32
- message += " (#{e.response})" if e.respond_to?(:response)
33
-
34
- raise Fluent::ConfigError, message
112
+ get_pods_and_start_watcher
113
+ rescue StandardError => e
114
+ message = 'start_pod_watch: Exception encountered setting up pod watch ' \
115
+ "from Kubernetes API #{@apiVersion} endpoint " \
116
+ "#{@kubernetes_url}: #{e.message}"
117
+ message += " (#{e.response})" if e.respond_to?(:response)
118
+ log.debug(message)
119
+
120
+ raise Fluent::ConfigError, message
121
+ end
122
+
123
+ # List all pods, record the resourceVersion and return a watcher starting
124
+ # from that resourceVersion.
125
+ def get_pods_and_start_watcher
126
+ options = {
127
+ resource_version: '0' # Fetch from API server cache instead of etcd quorum read
128
+ }
129
+ if ENV['K8S_NODE_NAME']
130
+ options[:field_selector] = 'spec.nodeName=' + ENV['K8S_NODE_NAME']
131
+ end
132
+ if @last_seen_resource_version
133
+ options[:resource_version] = @last_seen_resource_version
134
+ else
135
+ pods = @client.get_pods(options)
136
+ pods[:items].each do |pod|
137
+ cache_key = pod[:metadata][:uid]
138
+ @cache[cache_key] = parse_pod_metadata(pod)
139
+ @stats.bump(:pod_cache_host_updates)
140
+ end
141
+
142
+ # continue watching from most recent resourceVersion
143
+ options[:resource_version] = pods[:metadata][:resourceVersion]
35
144
  end
36
145
 
146
+ watcher = @client.watch_pods(options)
147
+ reset_pod_watch_retry_stats
148
+ watcher
149
+ end
150
+
151
+ # Reset pod watch retry count and backoff interval as there is a
152
+ # successful watch notice.
153
+ def reset_pod_watch_retry_stats
154
+ Thread.current[:pod_watch_retry_count] = 0
155
+ Thread.current[:pod_watch_retry_backoff_interval] = @watch_retry_interval
156
+ end
157
+
158
+ # Process a watcher notice and potentially raise an exception.
159
+ def process_pod_watcher_notices(watcher)
37
160
  watcher.each do |notice|
38
- case notice.type
39
- when 'MODIFIED'
40
- cache_key = notice.object['metadata']['uid']
41
- cached = @cache[cache_key]
42
- if cached
43
- @cache[cache_key] = parse_pod_metadata(notice.object)
44
- @stats.bump(:pod_cache_watch_updates)
45
- elsif ENV['K8S_NODE_NAME'] == notice.object['spec']['nodeName'] then
46
- @cache[cache_key] = parse_pod_metadata(notice.object)
47
- @stats.bump(:pod_cache_host_updates)
48
- else
49
- @stats.bump(:pod_cache_watch_misses)
50
- end
51
- when 'DELETED'
52
- # ignore and let age out for cases where pods
53
- # deleted but still processing logs
54
- @stats.bump(:pod_cache_watch_delete_ignored)
161
+ # store version we processed to not reprocess it ... do not unset when there is no version in response
162
+ version = ( # TODO: replace with &.dig once we are on ruby 2.5+
163
+ notice[:object] && notice[:object][:metadata] && notice[:object][:metadata][:resourceVersion]
164
+ )
165
+ @last_seen_resource_version = version if version
166
+
167
+ case notice[:type]
168
+ when 'MODIFIED'
169
+ reset_pod_watch_retry_stats
170
+ cache_key = notice.dig(:object, :metadata, :uid)
171
+ cached = @cache[cache_key]
172
+ if cached
173
+ @cache[cache_key] = parse_pod_metadata(notice[:object])
174
+ @stats.bump(:pod_cache_watch_updates)
175
+ elsif ENV['K8S_NODE_NAME'] == notice[:object][:spec][:nodeName]
176
+ @cache[cache_key] = parse_pod_metadata(notice[:object])
177
+ @stats.bump(:pod_cache_host_updates)
178
+ else
179
+ @stats.bump(:pod_cache_watch_misses)
180
+ end
181
+ when 'DELETED'
182
+ reset_pod_watch_retry_stats
183
+ # ignore and let age out for cases where pods
184
+ # deleted but still processing logs
185
+ @stats.bump(:pod_cache_watch_delete_ignored)
186
+ when 'ERROR'
187
+ if notice[:object] && notice[:object][:code] == 410
188
+ @last_seen_resource_version = nil # requested resourceVersion was too old, need to reset
189
+ @stats.bump(:pod_watch_gone_notices)
190
+ raise GoneError
55
191
  else
56
- # Don't pay attention to creations, since the created pod may not
57
- # end up on this node.
58
- @stats.bump(:pod_cache_watch_ignored)
192
+ @stats.bump(:pod_watch_error_type_notices)
193
+ message = notice[:object][:message] if notice[:object] && notice[:object][:message]
194
+ raise "Error while watching pods: #{message}"
195
+ end
196
+ else
197
+ reset_pod_watch_retry_stats
198
+ # Don't pay attention to creations, since the created pod may not
199
+ # end up on this node.
200
+ @stats.bump(:pod_cache_watch_ignored)
59
201
  end
60
202
  end
61
203
  end
data/release_notes.md ADDED
@@ -0,0 +1,42 @@
1
+ # Release Notes
2
+
3
+ ## 2.9.4
4
+ As of this release, the 'de_dot' functionality is depricated and will be removed in future releases.
5
+ Ref: https://github.com/fabric8io/fluent-plugin-kubernetes_metadata_filter/issues/320
6
+
7
+ ## v2.1.4
8
+ The use of `use_journal` is **DEPRECATED**. If this setting is not present, the plugin will
9
+ attempt to figure out the source of the metadata fields from the following:
10
+ - If `lookup_from_k8s_field true` (the default) and the following fields are present in the record:
11
+ `docker.container_id`, `kubernetes.namespace_name`, `kubernetes.pod_name`, `kubernetes.container_name`,
12
+ then the plugin will use those values as the source to use to lookup the metadata
13
+ - If `use_journal true`, or `use_journal` is unset, and the fields `CONTAINER_NAME` and `CONTAINER_ID_FULL` are present in the record,
14
+ then the plugin will parse those values using `container_name_to_kubernetes_regexp` and use those as the source to lookup the metadata
15
+ - Otherwise, if the tag matches `tag_to_kubernetes_name_regexp`, the plugin will parse the tag and use those values to
16
+ lookup the metdata
17
+
18
+ ## v2.1.x
19
+
20
+ As of the release 2.1.x of this plugin, it no longer supports parsing the source message into JSON and attaching it to the
21
+ payload. The following configuration options are removed:
22
+
23
+ * `merge_json_log`
24
+ * `preserve_json_log`
25
+
26
+ One way of preserving JSON logs can be through the [parser plugin](https://docs.fluentd.org/filter/parser).
27
+ It can parsed with the parser plugin like this:
28
+
29
+ ```
30
+ <filter kubernetes.**>
31
+ @type parser
32
+ key_name log
33
+ <parse>
34
+ @type json
35
+ json_parser json
36
+ </parse>
37
+ replace_invalid_sequence true
38
+ reserve_data true # this preserves unparsable log lines
39
+ emit_invalid_record_to_error false # In case of unparsable log lines keep the error log clean
40
+ reserve_time # the time was already parsed in the source, we don't want to overwrite it with current time.
41
+ </filter>
42
+ ```
@@ -18,40 +18,6 @@
18
18
  #
19
19
  ---
20
20
  http_interactions:
21
- - request:
22
- method: get
23
- uri: https://localhost:8443/api
24
- body:
25
- encoding: US-ASCII
26
- string: ''
27
- headers:
28
- Accept:
29
- - "*/*; q=0.5, application/xml"
30
- Accept-Encoding:
31
- - gzip, deflate
32
- User-Agent:
33
- - Ruby
34
- response:
35
- status:
36
- code: 200
37
- message: OK
38
- headers:
39
- Content-Type:
40
- - application/json
41
- Date:
42
- - Fri, 08 May 2015 10:35:37 GMT
43
- Content-Length:
44
- - '67'
45
- body:
46
- encoding: UTF-8
47
- string: |-
48
- {
49
- "versions": [
50
- "v1"
51
- ]
52
- }
53
- http_version:
54
- recorded_at: Fri, 08 May 2015 10:35:37 GMT
55
21
  - request:
56
22
  method: get
57
23
  uri: https://localhost:8443/api/v1/namespaces/default/pods/fabric8-console-controller-98rqc
@@ -18,40 +18,6 @@
18
18
  #
19
19
  ---
20
20
  http_interactions:
21
- - request:
22
- method: get
23
- uri: https://localhost:8443/api
24
- body:
25
- encoding: US-ASCII
26
- string: ''
27
- headers:
28
- Accept:
29
- - "*/*; q=0.5, application/xml"
30
- Accept-Encoding:
31
- - gzip, deflate
32
- User-Agent:
33
- - Ruby
34
- response:
35
- status:
36
- code: 200
37
- message: OK
38
- headers:
39
- Content-Type:
40
- - application/json
41
- Date:
42
- - Fri, 08 May 2015 10:35:37 GMT
43
- Content-Length:
44
- - '67'
45
- body:
46
- encoding: UTF-8
47
- string: |-
48
- {
49
- "versions": [
50
- "v1"
51
- ]
52
- }
53
- http_version:
54
- recorded_at: Fri, 08 May 2015 10:35:37 GMT
55
21
  - request:
56
22
  method: get
57
23
  uri: https://localhost:8443/api/v1/namespaces/default/pods/fabric8-console-controller-98rqc
@@ -0,0 +1,193 @@
1
+ #
2
+ # Fluentd Kubernetes Metadata Filter Plugin - Enrich Fluentd events with
3
+ # Kubernetes metadata
4
+ #
5
+ # Copyright 2015 Red Hat, Inc.
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+ ---
20
+ http_interactions:
21
+ - request:
22
+ method: get
23
+ uri: https://localhost:8443/api/v1
24
+ body:
25
+ encoding: US-ASCII
26
+ string: ''
27
+ headers:
28
+ Accept:
29
+ - "*/*; q=0.5, application/xml"
30
+ Accept-Encoding:
31
+ - gzip, deflate
32
+ User-Agent:
33
+ - Ruby
34
+ response:
35
+ status:
36
+ code: 200
37
+ message: OK
38
+ headers:
39
+ Content-Type:
40
+ - application/json
41
+ Date:
42
+ - Fri, 08 May 2015 10:35:37 GMT
43
+ Transfer-Encoding:
44
+ - chunked
45
+ body:
46
+ encoding: UTF-8
47
+ string: |-
48
+ {
49
+ "kind": "APIResourceList",
50
+ "groupVersion": "v1",
51
+ "resources": [
52
+ {
53
+ "name": "bindings",
54
+ "singularName": "",
55
+ "namespaced": true,
56
+ "kind": "Binding",
57
+ "verbs": [
58
+ "create"
59
+ ]
60
+ },
61
+ {
62
+ "name": "namespaces",
63
+ "singularName": "",
64
+ "namespaced": false,
65
+ "kind": "Namespace",
66
+ "verbs": [
67
+ "create",
68
+ "delete",
69
+ "get",
70
+ "list",
71
+ "patch",
72
+ "update",
73
+ "watch"
74
+ ],
75
+ "shortNames": [
76
+ "ns"
77
+ ]
78
+ },
79
+ {
80
+ "name": "namespaces/finalize",
81
+ "singularName": "",
82
+ "namespaced": false,
83
+ "kind": "Namespace",
84
+ "verbs": [
85
+ "update"
86
+ ]
87
+ },
88
+ {
89
+ "name": "namespaces/status",
90
+ "singularName": "",
91
+ "namespaced": false,
92
+ "kind": "Namespace",
93
+ "verbs": [
94
+ "get",
95
+ "patch",
96
+ "update"
97
+ ]
98
+ },
99
+ {
100
+ "name": "pods",
101
+ "singularName": "",
102
+ "namespaced": true,
103
+ "kind": "Pod",
104
+ "verbs": [
105
+ "create",
106
+ "delete",
107
+ "deletecollection",
108
+ "get",
109
+ "list",
110
+ "patch",
111
+ "update",
112
+ "watch"
113
+ ],
114
+ "shortNames": [
115
+ "po"
116
+ ],
117
+ "categories": [
118
+ "all"
119
+ ]
120
+ },
121
+ {
122
+ "name": "pods/attach",
123
+ "singularName": "",
124
+ "namespaced": true,
125
+ "kind": "Pod",
126
+ "verbs": []
127
+ },
128
+ {
129
+ "name": "pods/binding",
130
+ "singularName": "",
131
+ "namespaced": true,
132
+ "kind": "Binding",
133
+ "verbs": [
134
+ "create"
135
+ ]
136
+ },
137
+ {
138
+ "name": "pods/eviction",
139
+ "singularName": "",
140
+ "namespaced": true,
141
+ "group": "policy",
142
+ "version": "v1beta1",
143
+ "kind": "Eviction",
144
+ "verbs": [
145
+ "create"
146
+ ]
147
+ },
148
+ {
149
+ "name": "pods/exec",
150
+ "singularName": "",
151
+ "namespaced": true,
152
+ "kind": "Pod",
153
+ "verbs": []
154
+ },
155
+ {
156
+ "name": "pods/log",
157
+ "singularName": "",
158
+ "namespaced": true,
159
+ "kind": "Pod",
160
+ "verbs": [
161
+ "get"
162
+ ]
163
+ },
164
+ {
165
+ "name": "pods/portforward",
166
+ "singularName": "",
167
+ "namespaced": true,
168
+ "kind": "Pod",
169
+ "verbs": []
170
+ },
171
+ {
172
+ "name": "pods/proxy",
173
+ "singularName": "",
174
+ "namespaced": true,
175
+ "kind": "Pod",
176
+ "verbs": []
177
+ },
178
+ {
179
+ "name": "pods/status",
180
+ "singularName": "",
181
+ "namespaced": true,
182
+ "kind": "Pod",
183
+ "verbs": [
184
+ "get",
185
+ "patch",
186
+ "update"
187
+ ]
188
+ }
189
+ ]
190
+ }
191
+ http_version:
192
+ recorded_at: Fri, 08 May 2015 10:35:37 GMT
193
+ recorded_with: VCR 2.9.3