opentelemetry-instrumentation-redis 0.14.0 → 0.19.0
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/CHANGELOG.md +38 -11
- data/README.md +19 -2
- data/lib/opentelemetry/instrumentation/redis.rb +1 -0
- data/lib/opentelemetry/instrumentation/redis/instrumentation.rb +4 -1
- data/lib/opentelemetry/instrumentation/redis/patches/client.rb +58 -36
- data/lib/opentelemetry/instrumentation/redis/version.rb +1 -1
- metadata +25 -26
- data/lib/opentelemetry/instrumentation/redis/utils.rb +0 -65
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a358148a99762d16da53ed24dbdd31ebe490a73a3474dabf404a567de51feec
|
4
|
+
data.tar.gz: b3c86124ed7e9c63dc27057ed055c27a26bfa8e81fa04800c5ff362033141952
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '085395644553eb96d70ebb7fd26aab5dcd8489528faafacba229dab6ab31dac40dd6a29871e52e3d3be851559bc255d1017d4591386c39b9b1f0af6cb27baba3'
|
7
|
+
data.tar.gz: 8de51fbd8aabe59ea79277d59dc5ff003e23a8e812c58112fbef613b7ffacc7f58d7898d6af2678a15a30e828f891e058582f73c98c78a8ae8573c82547956d7
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,32 @@
|
|
1
1
|
# Release History: opentelemetry-instrumentation-redis
|
2
2
|
|
3
|
+
### v0.19.0 / 2021-05-28
|
4
|
+
|
5
|
+
* ADDED: Configuration option to enable or disable redis root spans [#777](https://github.com/open-telemetry/opentelemetry-ruby/pull/777)
|
6
|
+
|
7
|
+
### v0.18.0 / 2021-05-21
|
8
|
+
|
9
|
+
* ADDED: Updated API depedency for 1.0.0.rc1
|
10
|
+
refactor: redis attribute utils [#760](https://github.com/open-telemetry/opentelemetry-ruby/pull/760)
|
11
|
+
refactor: simplify redis attribute assignment [#758](https://github.com/open-telemetry/opentelemetry-ruby/pull/758)
|
12
|
+
test: split redis instrumentation test [#754](https://github.com/open-telemetry/opentelemetry-ruby/pull/754)
|
13
|
+
* ADDED: Option to obfuscate redis arguments
|
14
|
+
* FIXED: Instrument Redis more thoroughly by patching Client#process.
|
15
|
+
|
16
|
+
### v0.17.0 / 2021-04-22
|
17
|
+
|
18
|
+
* (No significant changes)
|
19
|
+
|
20
|
+
### v0.16.0 / 2021-03-17
|
21
|
+
|
22
|
+
* FIXED: Update DB semantic conventions
|
23
|
+
* FIXED: Example scripts now reference local common lib
|
24
|
+
* DOCS: Replace Gitter with GitHub Discussions
|
25
|
+
|
26
|
+
### v0.15.0 / 2021-02-18
|
27
|
+
|
28
|
+
* ADDED: Add instrumentation config validation
|
29
|
+
|
3
30
|
### v0.14.0 / 2021-02-03
|
4
31
|
|
5
32
|
* (No significant changes)
|
@@ -14,13 +41,13 @@
|
|
14
41
|
|
15
42
|
### v0.11.0 / 2020-12-11
|
16
43
|
|
17
|
-
* ADDED: Accept config for redis peer service attribute
|
18
|
-
* ADDED: Move utf8 encoding to common utils
|
19
|
-
* FIXED: Copyright comments to not reference year
|
44
|
+
* ADDED: Accept config for redis peer service attribute
|
45
|
+
* ADDED: Move utf8 encoding to common utils
|
46
|
+
* FIXED: Copyright comments to not reference year
|
20
47
|
|
21
48
|
### v0.10.1 / 2020-12-09
|
22
49
|
|
23
|
-
* FIXED: Semantic conventions db.type -> db.system
|
50
|
+
* FIXED: Semantic conventions db.type -> db.system
|
24
51
|
|
25
52
|
### v0.10.0 / 2020-12-03
|
26
53
|
|
@@ -28,21 +55,21 @@
|
|
28
55
|
|
29
56
|
### v0.9.0 / 2020-11-27
|
30
57
|
|
31
|
-
* BREAKING CHANGE: Add timeout for force_flush and shutdown
|
58
|
+
* BREAKING CHANGE: Add timeout for force_flush and shutdown
|
32
59
|
|
33
|
-
* ADDED: Redis attribute propagation
|
34
|
-
* ADDED: Add timeout for force_flush and shutdown
|
60
|
+
* ADDED: Redis attribute propagation
|
61
|
+
* ADDED: Add timeout for force_flush and shutdown
|
35
62
|
|
36
63
|
### v0.8.0 / 2020-10-27
|
37
64
|
|
38
|
-
* BREAKING CHANGE: Remove 'canonical' from status codes
|
65
|
+
* BREAKING CHANGE: Remove 'canonical' from status codes
|
39
66
|
|
40
|
-
* FIXED: Remove 'canonical' from status codes
|
67
|
+
* FIXED: Remove 'canonical' from status codes
|
41
68
|
|
42
69
|
### v0.7.0 / 2020-10-07
|
43
70
|
|
44
|
-
* DOCS: Added redis documentation
|
45
|
-
* DOCS: Standardize toplevel docs structure and readme
|
71
|
+
* DOCS: Added redis documentation
|
72
|
+
* DOCS: Standardize toplevel docs structure and readme
|
46
73
|
|
47
74
|
### v0.6.0 / 2020-09-10
|
48
75
|
|
data/README.md
CHANGED
@@ -41,15 +41,32 @@ OpenTelemetry::Instrumentation::Redis.with_attributes('peer.service' => 'cache')
|
|
41
41
|
end
|
42
42
|
```
|
43
43
|
|
44
|
+
### Configuration options
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
OpenTelemetry::SDK.configure do |c|
|
48
|
+
c.use 'OpenTelemetry::Instrumentation::Redis', {
|
49
|
+
# The obfuscation of arguments in the db.statement attribute is enabled by default.
|
50
|
+
# To disable, set enable_statement_obfuscation to false.
|
51
|
+
enable_statement_obfuscation: true,
|
52
|
+
}
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
44
56
|
## Example
|
45
57
|
|
46
58
|
An example of usage can be seen in [`example/redis.rb`](https://github.com/open-telemetry/opentelemetry-ruby/blob/main/instrumentation/redis/example/redis.rb).
|
47
59
|
|
60
|
+
## Development
|
61
|
+
|
62
|
+
You'll need Redis installed locally to run the test suite. Once you've
|
63
|
+
installed it, it will start and stop automatically when you run `rake`.
|
64
|
+
|
48
65
|
## How can I get involved?
|
49
66
|
|
50
67
|
The `opentelemetry-instrumentation-redis` gem source is [on github][repo-github], along with related gems including `opentelemetry-api` and `opentelemetry-sdk`.
|
51
68
|
|
52
|
-
The OpenTelemetry Ruby gems are maintained by the OpenTelemetry-Ruby special interest group (SIG). You can get involved by joining us
|
69
|
+
The OpenTelemetry Ruby gems are maintained by the OpenTelemetry-Ruby special interest group (SIG). You can get involved by joining us in [GitHub Discussions][discussions-url] or attending our weekly meeting. See the [meeting calendar][community-meetings] for dates and times. For more information on this and other language SIGs, see the OpenTelemetry [community page][ruby-sig].
|
53
70
|
|
54
71
|
## License
|
55
72
|
|
@@ -61,4 +78,4 @@ Apache 2.0 license. See [LICENSE][license-github] for more information.
|
|
61
78
|
[license-github]: https://github.com/open-telemetry/opentelemetry-ruby/blob/main/LICENSE
|
62
79
|
[ruby-sig]: https://github.com/open-telemetry/community#ruby-sig
|
63
80
|
[community-meetings]: https://github.com/open-telemetry/community#community-meetings
|
64
|
-
[
|
81
|
+
[discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions
|
@@ -19,10 +19,13 @@ module OpenTelemetry
|
|
19
19
|
defined?(::Redis)
|
20
20
|
end
|
21
21
|
|
22
|
+
option :peer_service, default: nil, validate: :string
|
23
|
+
option :trace_root_spans, default: true, validate: :boolean
|
24
|
+
option :enable_statement_obfuscation, default: true, validate: :boolean
|
25
|
+
|
22
26
|
private
|
23
27
|
|
24
28
|
def require_dependencies
|
25
|
-
require_relative 'utils'
|
26
29
|
require_relative 'patches/client'
|
27
30
|
end
|
28
31
|
|
@@ -10,53 +10,75 @@ module OpenTelemetry
|
|
10
10
|
module Patches
|
11
11
|
# Module to prepend to Redis::Client for instrumentation
|
12
12
|
module Client
|
13
|
-
|
14
|
-
|
13
|
+
MAX_STATEMENT_LENGTH = 500
|
14
|
+
private_constant :MAX_STATEMENT_LENGTH
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
tracer.in_span(
|
19
|
-
Utils.format_command(args),
|
20
|
-
attributes: attributes,
|
21
|
-
kind: :client
|
22
|
-
) do
|
23
|
-
response = super(*args, &block)
|
24
|
-
end
|
25
|
-
|
26
|
-
response
|
27
|
-
end
|
28
|
-
|
29
|
-
def call_pipeline(*args, &block)
|
30
|
-
response = nil
|
31
|
-
|
32
|
-
attributes = client_attributes
|
33
|
-
attributes['db.statement'] = Utils.format_pipeline_statement(args)
|
34
|
-
tracer.in_span(
|
35
|
-
'pipeline',
|
36
|
-
attributes: attributes,
|
37
|
-
kind: :client
|
38
|
-
) do
|
39
|
-
response = super(*args, &block)
|
40
|
-
end
|
16
|
+
def process(commands) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
17
|
+
return super unless config[:trace_root_spans] || OpenTelemetry::Trace.current_span.context.valid?
|
41
18
|
|
42
|
-
response
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def client_attributes
|
48
19
|
host = options[:host]
|
49
20
|
port = options[:port]
|
50
21
|
|
51
22
|
attributes = {
|
52
23
|
'db.system' => 'redis',
|
53
|
-
'db.instance' => options[:db].to_s,
|
54
|
-
'db.url' => "redis://#{host}:#{port}",
|
55
24
|
'net.peer.name' => host,
|
56
25
|
'net.peer.port' => port
|
57
26
|
}
|
27
|
+
|
28
|
+
attributes['db.redis.database_index'] = options[:db] unless options[:db].zero?
|
58
29
|
attributes['peer.service'] = config[:peer_service] if config[:peer_service]
|
59
|
-
attributes.merge(OpenTelemetry::Instrumentation::Redis.attributes)
|
30
|
+
attributes.merge!(OpenTelemetry::Instrumentation::Redis.attributes)
|
31
|
+
|
32
|
+
parsed_commands = parse_commands(commands)
|
33
|
+
parsed_commands = OpenTelemetry::Common::Utilities.truncate(parsed_commands, MAX_STATEMENT_LENGTH)
|
34
|
+
parsed_commands = OpenTelemetry::Common::Utilities.utf8_encode(parsed_commands, binary: true)
|
35
|
+
attributes['db.statement'] = parsed_commands
|
36
|
+
|
37
|
+
span_name = if commands.length == 1
|
38
|
+
commands[0][0].to_s.upcase
|
39
|
+
else
|
40
|
+
'PIPELINED'
|
41
|
+
end
|
42
|
+
|
43
|
+
tracer.in_span(span_name, attributes: attributes, kind: :client) do |s|
|
44
|
+
super(commands).tap do |reply|
|
45
|
+
if reply.is_a?(::Redis::CommandError)
|
46
|
+
s.record_exception(reply)
|
47
|
+
s.status = Trace::Status.new(
|
48
|
+
Trace::Status::ERROR,
|
49
|
+
description: reply.message
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Examples of commands received for parsing
|
59
|
+
# Redis#queue [[[:set, "v1", "0"]], [[:incr, "v1"]], [[:get, "v1"]]]
|
60
|
+
# Redis#pipeline: [[:set, "v1", "0"], [:incr, "v1"], [:get, "v1"]]
|
61
|
+
# Redis#hmset [[:hmset, "hash", "f1", 1234567890.0987654]]
|
62
|
+
# Redis#set [[:set, "K", "x"]]
|
63
|
+
def parse_commands(commands) # rubocop:disable Metrics/AbcSize
|
64
|
+
commands.map do |command|
|
65
|
+
# We are checking for the use of Redis#queue command, if we detect the
|
66
|
+
# extra level of array nesting we return the first element so it
|
67
|
+
# can be parsed.
|
68
|
+
command = command[0] if command.is_a?(Array) && command[0].is_a?(Array)
|
69
|
+
|
70
|
+
# If we receive an authentication request command
|
71
|
+
# we want to short circuit parsing the commands
|
72
|
+
# and return the obfuscated command
|
73
|
+
return 'AUTH ?' if command[0] == :auth
|
74
|
+
|
75
|
+
command[0] = command[0].to_s.upcase
|
76
|
+
if config[:enable_statement_obfuscation]
|
77
|
+
command[0] + ' ?' * (command.size - 1)
|
78
|
+
else
|
79
|
+
command.join(' ')
|
80
|
+
end
|
81
|
+
end.join("\n")
|
60
82
|
end
|
61
83
|
|
62
84
|
def tracer
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: opentelemetry-instrumentation-redis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.19.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- OpenTelemetry Authors
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-05-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: opentelemetry-api
|
@@ -16,70 +16,70 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 1.0.0.rc1
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 1.0.0.rc1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: opentelemetry-common
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.
|
33
|
+
version: 0.18.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.
|
40
|
+
version: 0.18.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: opentelemetry-instrumentation-base
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
48
|
-
type: :
|
47
|
+
version: 0.18.0
|
48
|
+
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.18.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: appraisal
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 2.2.0
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 2.2.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: bundler
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - "
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
75
|
+
version: '1.17'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - "
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
82
|
+
version: '1.17'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: minitest
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,14 +100,14 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
103
|
+
version: 1.0.0.rc1
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
110
|
+
version: 1.0.0.rc1
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: redis
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -194,16 +194,15 @@ files:
|
|
194
194
|
- lib/opentelemetry/instrumentation/redis.rb
|
195
195
|
- lib/opentelemetry/instrumentation/redis/instrumentation.rb
|
196
196
|
- lib/opentelemetry/instrumentation/redis/patches/client.rb
|
197
|
-
- lib/opentelemetry/instrumentation/redis/utils.rb
|
198
197
|
- lib/opentelemetry/instrumentation/redis/version.rb
|
199
198
|
homepage: https://github.com/open-telemetry/opentelemetry-ruby
|
200
199
|
licenses:
|
201
200
|
- Apache-2.0
|
202
201
|
metadata:
|
203
|
-
changelog_uri: https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-instrumentation-redis/v0.
|
202
|
+
changelog_uri: https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-instrumentation-redis/v0.19.0/file.CHANGELOG.html
|
204
203
|
source_code_uri: https://github.com/open-telemetry/opentelemetry-ruby/tree/main/instrumentation/redis
|
205
204
|
bug_tracker_uri: https://github.com/open-telemetry/opentelemetry-ruby/issues
|
206
|
-
documentation_uri: https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-instrumentation-redis/v0.
|
205
|
+
documentation_uri: https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-instrumentation-redis/v0.19.0
|
207
206
|
post_install_message:
|
208
207
|
rdoc_options: []
|
209
208
|
require_paths:
|
@@ -219,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
219
218
|
- !ruby/object:Gem::Version
|
220
219
|
version: '0'
|
221
220
|
requirements: []
|
222
|
-
rubygems_version: 3.1.
|
221
|
+
rubygems_version: 3.1.6
|
223
222
|
signing_key:
|
224
223
|
specification_version: 4
|
225
224
|
summary: Redis instrumentation for the OpenTelemetry framework
|
@@ -1,65 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Copyright The OpenTelemetry Authors
|
4
|
-
#
|
5
|
-
# SPDX-License-Identifier: Apache-2.0
|
6
|
-
|
7
|
-
module OpenTelemetry
|
8
|
-
module Instrumentation
|
9
|
-
module Redis
|
10
|
-
# Utility functions
|
11
|
-
module Utils
|
12
|
-
extend self
|
13
|
-
|
14
|
-
PLACEHOLDER = '?'
|
15
|
-
VALUE_MAX_LEN = 50
|
16
|
-
CMD_MAX_LEN = 500
|
17
|
-
|
18
|
-
def format_command(command_args)
|
19
|
-
format_arg(resolve_command_args(command_args).first)
|
20
|
-
end
|
21
|
-
|
22
|
-
def format_pipeline_statement(command_args)
|
23
|
-
command_args[0].commands.map do |args|
|
24
|
-
format_statement(args)
|
25
|
-
end.join("\n")
|
26
|
-
end
|
27
|
-
|
28
|
-
def format_statement(command_args)
|
29
|
-
command_args = resolve_command_args(command_args)
|
30
|
-
return 'AUTH ?' if auth_command?(command_args)
|
31
|
-
|
32
|
-
cmd = command_args.map { |x| format_arg(x) }.join(' ')
|
33
|
-
truncate(cmd, CMD_MAX_LEN)
|
34
|
-
end
|
35
|
-
|
36
|
-
def format_arg(arg)
|
37
|
-
str = arg.is_a?(Symbol) ? arg.to_s.upcase : arg.to_s
|
38
|
-
str = OpenTelemetry::Common::Utilities.utf8_encode(str, binary: true)
|
39
|
-
truncate(str, VALUE_MAX_LEN)
|
40
|
-
rescue StandardError => e
|
41
|
-
OpenTelemetry.logger.debug("non formattable Redis arg #{str}: #{e}")
|
42
|
-
PLACEHOLDER
|
43
|
-
end
|
44
|
-
|
45
|
-
def auth_command?(command_args)
|
46
|
-
return false unless command_args.is_a?(Array) && !command_args.empty?
|
47
|
-
|
48
|
-
command_args.first.to_sym == :auth
|
49
|
-
end
|
50
|
-
|
51
|
-
# Unwraps command array when Redis is called with the following syntax:
|
52
|
-
# redis.call([:cmd, 'arg1', ...])
|
53
|
-
def resolve_command_args(command_args)
|
54
|
-
return command_args.first if command_args.is_a?(Array) && command_args.first.is_a?(Array)
|
55
|
-
|
56
|
-
command_args
|
57
|
-
end
|
58
|
-
|
59
|
-
def truncate(string, size)
|
60
|
-
string.size > size ? "#{string[0...size - 3]}..." : string
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|