fluent-plugin-lm-logs 1.2.5 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +31 -1
- data/lib/fluent/plugin/out_lm.rb +223 -219
- data/lib/fluent/plugin/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8458c35bd163ce3c82f8c7e71e9d83450e1063bee51f6f497f3a6714c7269fc1
|
4
|
+
data.tar.gz: 9ebae5b64a71e61b335f957ec61e3ceb27d766e5e4af1a634247845f082d5503
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e85a136c8d024082707e7daf0109de9f026ec2916a370c12e8a3b58b0fe7725e09601550fa2bd2d6a366c5e4fa63843589010b87f9642164962e26e24d174c65
|
7
|
+
data.tar.gz: 4b2e8b6b36c4baa54443068c9af875db9d93e57231bebcb48b0e419bd9fb68704c7da0dd9cd54901916cbf33f820f27f08e8ba82193056335e70495445539c01
|
data/README.md
CHANGED
@@ -23,8 +23,9 @@ Create a custom `fluent.conf` or edit the existing one to specify which logs sho
|
|
23
23
|
resource_mapping {"<event_key>": "<lm_property>"}
|
24
24
|
company_name <lm_company_name>
|
25
25
|
company_domain <lm_company_domain>
|
26
|
-
|
26
|
+
access_id <lm_access_id>
|
27
27
|
access_key <lm_access_key>
|
28
|
+
resource_type <resource_type>
|
28
29
|
<buffer>
|
29
30
|
@type memory
|
30
31
|
flush_interval 1s
|
@@ -33,6 +34,34 @@ Create a custom `fluent.conf` or edit the existing one to specify which logs sho
|
|
33
34
|
debug false
|
34
35
|
</match>
|
35
36
|
```
|
37
|
+
### Dynamic resource type
|
38
|
+
|
39
|
+
If you want to use a dynamic resource type, you can leave the `resource_type` field empty. The plugin will then automatically assign the resource type based on either of below:
|
40
|
+
* If user assigns source-specific tags as below:
|
41
|
+
```
|
42
|
+
Tag windows.server1.logs
|
43
|
+
Tag linux.vm02.logs
|
44
|
+
```
|
45
|
+
* In the fluentd conf file:
|
46
|
+
```
|
47
|
+
<filter **>
|
48
|
+
@type record_transformer
|
49
|
+
enable_ruby true
|
50
|
+
<record>
|
51
|
+
resource_type ${record["resource_type"] || "Unknown"}
|
52
|
+
</record>
|
53
|
+
</filter>
|
54
|
+
```
|
55
|
+
* If the remote agent includes a host field (many do), we can use heuristics:
|
56
|
+
```
|
57
|
+
|
58
|
+
host = record['host'] || record['hostname'] || ''
|
59
|
+
return 'AWS/VirtualMachine' if host.start_with?('ip-')
|
60
|
+
return 'GCP/VirtualMachine' if host.include?('.c.') || host.include?('gcp')
|
61
|
+
return 'WindowsServer' if host.include?('win')
|
62
|
+
return 'LinuxServer' if host.include?('linux')
|
63
|
+
'Unknown'
|
64
|
+
```
|
36
65
|
|
37
66
|
### Request example
|
38
67
|
|
@@ -68,6 +97,7 @@ See the [LogicMonitor Helm repository](https://github.com/logicmonitor/k8s-helm-
|
|
68
97
|
| `resource_mapping` | The mapping that defines the source of the log event to the LM resource. In this case, the `<event_key>` in the incoming event is mapped to the value of `<lm_property>`.|
|
69
98
|
| `access_id` | LM API Token access ID. |
|
70
99
|
| `access_key` | LM API Token access key. |
|
100
|
+
| `resource_type` | If a Resource Type is specified, it will be statically applied to all ingested logs. If left blank, a dynamic Resource Type will be assigned. |
|
71
101
|
| `bearer_token` | LM API Bearer Token. Either specify `access_id` and `access_key` both or `bearer_token`. If all specified, LMv1 token(`access_id` and `access_key`) will be used for authentication with Logicmonitor. |
|
72
102
|
| `flush_interval` | Defines the time in seconds to wait before sending batches of logs to LogicMonitor. Default is `60s`. |
|
73
103
|
| `debug` | When `true`, logs more information to the fluentd console. |
|
data/lib/fluent/plugin/out_lm.rb
CHANGED
@@ -17,286 +17,290 @@ require_relative "version"
|
|
17
17
|
|
18
18
|
|
19
19
|
module Fluent
|
20
|
-
|
21
|
-
Fluent::Plugin
|
20
|
+
module Plugin
|
21
|
+
class LmOutput < Fluent::Plugin::Output
|
22
|
+
Fluent::Plugin.register_output('lm', self)
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
RESOURCE_MAPPING_KEY = "_lm.resourceId".freeze
|
25
|
+
DEVICELESS_KEY_SERVICE = "resource.service.name".freeze
|
26
|
+
DEVICELESS_KEY_NAMESPACE = "resource.service.namespace".freeze
|
26
27
|
|
27
|
-
|
28
|
+
# config_param defines a parameter. You can refer a parameter via @path instance variable
|
28
29
|
|
29
|
-
|
30
|
+
config_param :access_id, :string, :default => nil
|
30
31
|
|
31
|
-
|
32
|
+
config_param :access_key, :string, :default => nil, secret: true
|
32
33
|
|
33
|
-
|
34
|
+
config_param :company_name, :string, :default => "company_name"
|
34
35
|
|
35
|
-
|
36
|
+
config_param :resource_mapping, :hash, :default => {"host": "system.hostname", "hostname": "system.hostname"}
|
36
37
|
|
37
|
-
|
38
|
+
config_param :debug, :bool, :default => false
|
38
39
|
|
39
|
-
|
40
|
+
config_param :include_metadata, :bool, :default => false
|
40
41
|
|
41
|
-
|
42
|
+
config_param :force_encoding, :string, :default => ""
|
42
43
|
|
43
|
-
|
44
|
+
config_param :compression, :string, :default => ""
|
44
45
|
|
45
|
-
|
46
|
+
config_param :log_source, :string, :default => "lm-logs-fluentd"
|
46
47
|
|
47
|
-
|
48
|
+
config_param :version_id, :string, :default => "version_id"
|
48
49
|
|
49
|
-
|
50
|
+
config_param :device_less_logs, :bool, :default => false
|
50
51
|
|
51
|
-
|
52
|
+
config_param :http_proxy, :string, :default => nil
|
52
53
|
|
53
|
-
|
54
|
+
config_param :company_domain , :string, :default => "logicmonitor.com"
|
54
55
|
|
55
|
-
|
56
|
+
config_param :resource_type, :string, :default => ""
|
57
|
+
# Use bearer token for auth.
|
58
|
+
config_param :bearer_token, :string, :default => nil, secret: true
|
56
59
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
def configure(conf)
|
64
|
-
super
|
65
|
-
end
|
60
|
+
# This method is called before starting.
|
61
|
+
# 'conf' is a Hash that includes configuration parameters.
|
62
|
+
# If the configuration is invalid, raise Fluent::ConfigError.
|
63
|
+
def configure(conf)
|
64
|
+
super
|
65
|
+
end
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
|
67
|
+
def multi_workers_ready?
|
68
|
+
true
|
69
|
+
end
|
70
70
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
71
|
+
# This method is called when starting.
|
72
|
+
# Open sockets or files here.
|
73
|
+
def start
|
74
|
+
super
|
75
|
+
configure_auth
|
76
|
+
proxy_uri = :ENV
|
77
|
+
if @http_proxy
|
78
|
+
proxy_uri = URI.parse(http_proxy)
|
79
|
+
elsif ENV['HTTP_PROXY'] || ENV['http_proxy']
|
80
|
+
log.info("Using HTTP proxy defined in environment variable")
|
81
|
+
end
|
82
|
+
@http_client = Net::HTTP::Persistent.new name: "fluent-plugin-lm-logs", proxy: proxy_uri
|
83
|
+
@http_client.override_headers["Content-Type"] = "application/json"
|
84
|
+
@http_client.override_headers["User-Agent"] = log_source + "/" + LmLogsFluentPlugin::VERSION
|
85
|
+
@url = "https://#{@company_name}.#{@company_domain}/rest/log/ingest"
|
86
|
+
@uri = URI.parse(@url)
|
87
|
+
@detector = EnvironmentDetector.new
|
88
|
+
@environment_info = @detector.detect
|
89
|
+
@local_env_str = format_environment(@environment_info)
|
90
|
+
|
91
|
+
log.info("Environment detected: #{@environment_info}")
|
81
92
|
end
|
82
|
-
@http_client = Net::HTTP::Persistent.new name: "fluent-plugin-lm-logs", proxy: proxy_uri
|
83
|
-
@http_client.override_headers["Content-Type"] = "application/json"
|
84
|
-
@http_client.override_headers["User-Agent"] = log_source + "/" + LmLogsFluentPlugin::VERSION
|
85
|
-
@url = "https://#{@company_name}.#{@company_domain}/rest/log/ingest"
|
86
|
-
@uri = URI.parse(@url)
|
87
|
-
@detector = EnvironmentDetector.new
|
88
|
-
@environment_info = @detector.detect
|
89
|
-
@local_env_str = format_environment(@environment_info)
|
90
|
-
|
91
|
-
log.info("Environment detected: #{@environment_info}")
|
92
|
-
end
|
93
93
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
94
|
+
def configure_auth
|
95
|
+
@use_bearer_instead_of_lmv1 = false
|
96
|
+
if is_blank(@access_id) || is_blank(@access_key)
|
97
|
+
log.info "Access Id or access key blank / null. Using bearer token for authentication."
|
98
|
+
@use_bearer_instead_of_lmv1 = true
|
99
|
+
end
|
100
|
+
if @use_bearer_instead_of_lmv1 && is_blank(@bearer_token)
|
101
|
+
log.error "Bearer token not specified. Either access_id and access_key both or bearer_token must be specified for authentication with Logicmonitor."
|
102
|
+
raise ArgumentError, 'No valid authentication specified. Either access_id and access_key both or bearer_token must be specified for authentication with Logicmonitor.'
|
103
|
+
end
|
99
104
|
end
|
100
|
-
|
101
|
-
|
102
|
-
|
105
|
+
# This method is called when shutting down.
|
106
|
+
# Shutdown the thread and close sockets or files here.
|
107
|
+
def shutdown
|
108
|
+
super
|
109
|
+
@http_client.shutdown
|
103
110
|
end
|
104
|
-
end
|
105
|
-
# This method is called when shutting down.
|
106
|
-
# Shutdown the thread and close sockets or files here.
|
107
|
-
def shutdown
|
108
|
-
super
|
109
|
-
@http_client.shutdown
|
110
|
-
end
|
111
111
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
112
|
+
# This method is called when an event reaches to Fluentd.
|
113
|
+
# Convert the event to a raw string.
|
114
|
+
def format(tag, time, record)
|
115
|
+
[tag, time, record].to_msgpack
|
116
|
+
end
|
117
117
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
118
|
+
# This method is called every flush interval. Write the buffer chunk
|
119
|
+
# to files or databases here.
|
120
|
+
# 'chunk' is a buffer chunk that includes multiple formatted
|
121
|
+
# events. You can use 'data = chunk.read' to get all events and
|
122
|
+
# 'chunk.open {|io| ... }' to get IO objects.
|
123
|
+
#
|
124
|
+
# NOTE! This method is called by internal thread, not Fluentd's main thread. So IO wait doesn't affect other plugins.
|
125
|
+
def write(chunk)
|
126
|
+
events = []
|
127
|
+
chunk.msgpack_each do |(tag, time, record)|
|
128
|
+
event = process_record(tag,time,record)
|
129
|
+
if event != nil
|
130
|
+
events.push(event)
|
131
|
+
end
|
131
132
|
end
|
133
|
+
send_batch(events)
|
132
134
|
end
|
133
|
-
send_batch(events)
|
134
|
-
end
|
135
135
|
|
136
|
-
|
137
|
-
|
138
|
-
lm_event = {}
|
139
|
-
|
140
|
-
if @include_metadata
|
141
|
-
lm_event = get_metadata(record)
|
136
|
+
def formatted_to_msgpack_binary?
|
137
|
+
true
|
142
138
|
end
|
143
139
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
140
|
+
def process_record(tag, time, record)
|
141
|
+
resource_map = {}
|
142
|
+
lm_event = {}
|
143
|
+
|
144
|
+
if @include_metadata
|
145
|
+
lm_event = get_metadata(record)
|
146
|
+
end
|
147
|
+
|
148
|
+
if !@device_less_logs
|
149
|
+
# With devices
|
150
|
+
if record[RESOURCE_MAPPING_KEY] == nil
|
151
|
+
@resource_mapping.each do |key, value|
|
152
|
+
k = value
|
153
|
+
nestedVal = record
|
154
|
+
key.to_s.split('.').each { |x| nestedVal = nestedVal[x] }
|
155
|
+
if nestedVal != nil
|
156
|
+
resource_map[k] = nestedVal
|
157
|
+
end
|
153
158
|
end
|
154
|
-
|
155
|
-
|
159
|
+
lm_event[RESOURCE_MAPPING_KEY] = resource_map
|
160
|
+
else
|
161
|
+
lm_event[RESOURCE_MAPPING_KEY] = record[RESOURCE_MAPPING_KEY]
|
162
|
+
end
|
156
163
|
else
|
157
|
-
|
164
|
+
# Device less
|
165
|
+
if record[DEVICELESS_KEY_SERVICE]==nil
|
166
|
+
log.error "When device_less_logs is set \'true\', record must have \'service\'. Ignoring this event #{lm_event}."
|
167
|
+
return nil
|
168
|
+
else
|
169
|
+
lm_event[DEVICELESS_KEY_SERVICE] = encode_if_necessary(record[DEVICELESS_KEY_SERVICE])
|
170
|
+
if record[DEVICELESS_KEY_NAMESPACE]!=nil
|
171
|
+
lm_event[DEVICELESS_KEY_NAMESPACE] = encode_if_necessary(record[DEVICELESS_KEY_NAMESPACE])
|
172
|
+
end
|
173
|
+
end
|
158
174
|
end
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
log.error "When device_less_logs is set \'true\', record must have \'service\'. Ignoring this event #{lm_event}."
|
163
|
-
return nil
|
175
|
+
|
176
|
+
if record["timestamp"] != nil
|
177
|
+
lm_event["timestamp"] = record["timestamp"]
|
164
178
|
else
|
165
|
-
lm_event[
|
166
|
-
if record[DEVICELESS_KEY_NAMESPACE]!=nil
|
167
|
-
lm_event[DEVICELESS_KEY_NAMESPACE] = encode_if_necessary(record[DEVICELESS_KEY_NAMESPACE])
|
168
|
-
end
|
179
|
+
lm_event["timestamp"] = Time.at(time).utc.to_datetime.rfc3339
|
169
180
|
end
|
170
|
-
|
181
|
+
lm_event["message"] = encode_if_necessary(record["message"])
|
171
182
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
end
|
177
|
-
lm_event["message"] = encode_if_necessary(record["message"])
|
183
|
+
resource_type = @resource_type || @detector.infer_resource_type(record, tag)
|
184
|
+
if resource_type.nil? || resource_type.strip.empty? || resource_type == 'Unknown'
|
185
|
+
resource_type = @local_env_str
|
186
|
+
end
|
178
187
|
|
179
|
-
|
180
|
-
if resource_type.nil? || resource_type.strip.empty? || resource_type == 'Unknown'
|
181
|
-
resource_type = @local_env_str
|
182
|
-
end
|
188
|
+
lm_event['_resource.type'] = resource_type
|
183
189
|
|
184
|
-
|
190
|
+
return lm_event
|
191
|
+
end
|
185
192
|
|
186
|
-
|
187
|
-
|
193
|
+
def get_metadata(record)
|
194
|
+
#if encoding is not defined we will skip going through each key val
|
195
|
+
#and return the whole record for performance reasons in case of a bulky record.
|
196
|
+
if @force_encoding == ""
|
197
|
+
return record
|
198
|
+
else
|
199
|
+
lm_event = {}
|
200
|
+
record.each do |key, value|
|
201
|
+
lm_event["#{key}"] = get_encoded_string(value)
|
202
|
+
end
|
203
|
+
return lm_event
|
204
|
+
end
|
205
|
+
end
|
188
206
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
else
|
195
|
-
lm_event = {}
|
196
|
-
record.each do |key, value|
|
197
|
-
lm_event["#{key}"] = get_encoded_string(value)
|
207
|
+
def encode_if_necessary(str)
|
208
|
+
if @force_encoding != ""
|
209
|
+
return get_encoded_string(str)
|
210
|
+
else
|
211
|
+
return str
|
198
212
|
end
|
199
|
-
return lm_event
|
200
213
|
end
|
201
|
-
end
|
202
214
|
|
203
|
-
|
204
|
-
|
205
|
-
return get_encoded_string(str)
|
206
|
-
else
|
207
|
-
return str
|
215
|
+
def get_encoded_string(str)
|
216
|
+
return str.force_encoding(@force_encoding).encode("UTF-8")
|
208
217
|
end
|
209
|
-
end
|
210
218
|
|
211
|
-
|
212
|
-
|
213
|
-
end
|
219
|
+
def send_batch(events)
|
220
|
+
body = events.to_json
|
214
221
|
|
215
|
-
|
216
|
-
|
222
|
+
if @debug
|
223
|
+
log.info "Sending #{events.length} events to logic monitor at #{@url}"
|
224
|
+
log.info "Request json #{body}"
|
225
|
+
end
|
217
226
|
|
218
|
-
|
219
|
-
|
220
|
-
log.info "Request json #{body}"
|
221
|
-
end
|
227
|
+
request = Net::HTTP::Post.new(@uri.request_uri)
|
228
|
+
request['authorization'] = generate_token(events)
|
222
229
|
|
223
|
-
|
224
|
-
|
230
|
+
if @compression == "gzip"
|
231
|
+
request['Content-Encoding'] = "gzip"
|
232
|
+
gzip = Zlib::GzipWriter.new(StringIO.new)
|
233
|
+
gzip << body
|
234
|
+
request.body = gzip.close.string
|
235
|
+
else
|
236
|
+
request.body = body
|
237
|
+
end
|
225
238
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
request.body = gzip.close.string
|
231
|
-
else
|
232
|
-
request.body = body
|
233
|
-
end
|
239
|
+
if @debug
|
240
|
+
log.info "Sending the below request headers to logicmonitor:"
|
241
|
+
request.each_header {|key,value| log.info "#{key} = #{value}" }
|
242
|
+
end
|
234
243
|
|
235
|
-
|
236
|
-
|
237
|
-
|
244
|
+
resp = @http_client.request @uri, request
|
245
|
+
if @debug || resp.kind_of?(Net::HTTPMultiStatus) || !resp.kind_of?(Net::HTTPSuccess)
|
246
|
+
log.info "Status code:#{resp.code} Request Id:#{resp.header['x-request-id']} message:#{resp.body}"
|
247
|
+
end
|
238
248
|
end
|
239
249
|
|
240
|
-
resp = @http_client.request @uri, request
|
241
|
-
if @debug || resp.kind_of?(Net::HTTPMultiStatus) || !resp.kind_of?(Net::HTTPSuccess)
|
242
|
-
log.info "Status code:#{resp.code} Request Id:#{resp.header['x-request-id']} message:#{resp.body}"
|
243
|
-
end
|
244
|
-
end
|
245
250
|
|
251
|
+
def generate_token(events)
|
246
252
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
return "LMv1 #{@access_id}:#{signature}:#{timestamp}"
|
253
|
+
if @use_bearer_instead_of_lmv1
|
254
|
+
return "Bearer #{@bearer_token}"
|
255
|
+
else
|
256
|
+
timestamp = DateTime.now.strftime('%Q')
|
257
|
+
signature = Base64.strict_encode64(
|
258
|
+
OpenSSL::HMAC.hexdigest(
|
259
|
+
OpenSSL::Digest.new('sha256'),
|
260
|
+
@access_key,
|
261
|
+
"POST#{timestamp}#{events.to_json}/log/ingest"
|
262
|
+
)
|
263
|
+
)
|
264
|
+
return "LMv1 #{@access_id}:#{signature}:#{timestamp}"
|
265
|
+
end
|
261
266
|
end
|
262
|
-
end
|
263
267
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
268
|
+
def is_blank(str)
|
269
|
+
if str.nil? || str.to_s.strip.empty?
|
270
|
+
return true
|
271
|
+
else
|
272
|
+
return false
|
273
|
+
end
|
269
274
|
end
|
270
|
-
end
|
271
275
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
276
|
+
def format_environment(env_info)
|
277
|
+
runtime = env_info[:runtime]
|
278
|
+
provider = env_info[:provider] if env_info.key?(:provider)
|
279
|
+
|
280
|
+
case runtime
|
281
|
+
when 'kubernetes'
|
282
|
+
'Kubernetes/Node'
|
283
|
+
when 'docker'
|
284
|
+
'Docker/Host'
|
285
|
+
when 'vm'
|
286
|
+
case provider&.downcase
|
287
|
+
when 'azure'
|
288
|
+
'Azure/VirtualMachine'
|
289
|
+
when 'aws'
|
290
|
+
'AWS/EC2'
|
291
|
+
when 'gcp'
|
292
|
+
'GCP/ComputeEngine'
|
293
|
+
else
|
294
|
+
'Unknown/VirtualMachine'
|
295
|
+
end
|
296
|
+
when 'physical'
|
297
|
+
os = env_info[:os] || 'UnknownOS'
|
298
|
+
product = env_info[:product] || 'UnknownHardware'
|
299
|
+
"#{os} / #{product}"
|
289
300
|
else
|
290
|
-
'
|
301
|
+
'UnknownEnvironment'
|
291
302
|
end
|
292
|
-
when 'physical'
|
293
|
-
os = env_info[:os] || 'UnknownOS'
|
294
|
-
product = env_info[:product] || 'UnknownHardware'
|
295
|
-
"#{os} / #{product}"
|
296
|
-
else
|
297
|
-
'UnknownEnvironment'
|
298
303
|
end
|
299
304
|
end
|
300
|
-
|
301
305
|
end
|
302
306
|
end
|