prrd 0.2.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 +7 -0
- data/Gemfile +13 -0
- data/README.md +223 -0
- data/Rakefile +35 -0
- data/lib/prrd.rb +189 -0
- data/lib/prrd/database.rb +107 -0
- data/lib/prrd/database/archive.rb +39 -0
- data/lib/prrd/database/datasource.rb +40 -0
- data/lib/prrd/graph.rb +143 -0
- data/lib/prrd/graph/area.rb +50 -0
- data/lib/prrd/graph/colors.rb +28 -0
- data/lib/prrd/graph/comment.rb +29 -0
- data/lib/prrd/graph/definition.rb +47 -0
- data/lib/prrd/graph/hrule.rb +53 -0
- data/lib/prrd/graph/line.rb +53 -0
- data/lib/prrd/graph/print.rb +48 -0
- data/lib/prrd/graph/shift.rb +30 -0
- data/lib/prrd/graph/textalign.rb +32 -0
- data/lib/prrd/graph/vrule.rb +51 -0
- data/prrd.gemspec +23 -0
- data/showcase.rb +104 -0
- data/spec/prrd_spec.rb +15 -0
- metadata +65 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 4c074fd1d609522279be5d2493075487a4b43403
|
|
4
|
+
data.tar.gz: 96b588b278c0f30d663ba3d8766b6e843d87936d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3d1c7eb14b07162eb2c35b49ebc32de72cadded2d54b9783921440aa317f772350252d0ce27ec54b621ff3ffcb1358b4c4074a2736917ac9670e9c28ac090c9f
|
|
7
|
+
data.tar.gz: f6e18d680ae236f3b0cf09a6d43a970bfef9b8ca82040ea4e8b368c65a1aa835c171b04743e688f04f9b6025a860cbc829b0f409d2166eb87e2161901684d16a
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# PRRD
|
|
2
|
+
|
|
3
|
+
A (simple) rrdtool ruby interface
|
|
4
|
+
|
|
5
|
+
## Disclaimer
|
|
6
|
+
|
|
7
|
+
Hold on please, work in progress ...
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
During the development process, there is no gem available. It will be packaged when the first stable release is reached.
|
|
12
|
+
|
|
13
|
+
To install PRRD, install dependencies and clone this repository:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
sudo gem install bundler
|
|
17
|
+
git clone https://github.com/pierre-lecocq/prrd
|
|
18
|
+
cd prrd
|
|
19
|
+
bundle install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Require the library
|
|
25
|
+
|
|
26
|
+
`require '/path/to/prrd/lib/prdd.rb'`
|
|
27
|
+
|
|
28
|
+
### Define a database
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
database = PRRD::Database.new
|
|
32
|
+
database.path = '/path/to/your/directory/sample.rrd'
|
|
33
|
+
|
|
34
|
+
unless database.exists?
|
|
35
|
+
|
|
36
|
+
database.start = Time.now.to_i
|
|
37
|
+
database.step = 300
|
|
38
|
+
|
|
39
|
+
# Add datasources
|
|
40
|
+
|
|
41
|
+
ds = PRRD::Database::Datasource.new
|
|
42
|
+
ds.name = 'ds1'
|
|
43
|
+
ds.type = 'GAUGE'
|
|
44
|
+
ds.heartbeat = 600
|
|
45
|
+
ds.min = 0
|
|
46
|
+
ds.max = 'U'
|
|
47
|
+
database.add_datasource ds
|
|
48
|
+
|
|
49
|
+
ds = PRRD::Database::Datasource.new
|
|
50
|
+
ds.name = 'ds2'
|
|
51
|
+
ds.type = 'GAUGE'
|
|
52
|
+
ds.heartbeat = 600
|
|
53
|
+
ds.min = 0
|
|
54
|
+
ds.max = 'U'
|
|
55
|
+
database.add_datasource ds
|
|
56
|
+
|
|
57
|
+
# Add archives
|
|
58
|
+
|
|
59
|
+
ar = PRRD::Database::Archive.new
|
|
60
|
+
ar.cf = 'AVERAGE'
|
|
61
|
+
ar.xff = 0.5
|
|
62
|
+
ar.steps = 1
|
|
63
|
+
ar.rows = 576
|
|
64
|
+
database.add_archive ar
|
|
65
|
+
|
|
66
|
+
ar = PRRD::Database::Archive.new
|
|
67
|
+
ar.cf = 'AVERAGE'
|
|
68
|
+
ar.xff = 0.5
|
|
69
|
+
ar.steps = 6
|
|
70
|
+
ar.rows = 672
|
|
71
|
+
database.add_archive ar
|
|
72
|
+
|
|
73
|
+
ar = PRRD::Database::Archive.new
|
|
74
|
+
ar.cf = 'AVERAGE'
|
|
75
|
+
ar.xff = 0.5
|
|
76
|
+
ar.steps = 24
|
|
77
|
+
ar.rows = 732
|
|
78
|
+
database.add_archive ar
|
|
79
|
+
|
|
80
|
+
ar = PRRD::Database::Archive.new
|
|
81
|
+
ar.cf = 'AVERAGE'
|
|
82
|
+
ar.xff = 0.5
|
|
83
|
+
ar.steps = 144
|
|
84
|
+
ar.rows = 1460
|
|
85
|
+
database.add_archive ar
|
|
86
|
+
|
|
87
|
+
# Create
|
|
88
|
+
|
|
89
|
+
database.create
|
|
90
|
+
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Update the database
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
value_ds1 = 125
|
|
98
|
+
value_ds2 = 38
|
|
99
|
+
|
|
100
|
+
database.update Time.now.to_i, value_ds1, value_ds2
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Generate the graph
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
graph = PRRD::Graph.new
|
|
108
|
+
graph.path = '/path/to/your/directory/sample.png'
|
|
109
|
+
graph.database = database
|
|
110
|
+
graph.width = 600
|
|
111
|
+
graph.height = 300
|
|
112
|
+
graph.title = 'My graph'
|
|
113
|
+
graph.vertical_align = 'My vertical label'
|
|
114
|
+
|
|
115
|
+
# Optionally set colors
|
|
116
|
+
|
|
117
|
+
graph.add_color PRRD::Graph::Color.new colortag: 'BACK', color: '#151515'
|
|
118
|
+
graph.add_color PRRD::Graph::Color.new colortag: 'FONT', color: '#e5e5e5'
|
|
119
|
+
graph.add_color PRRD::Graph::Color.new colortag: 'CANVAS', color: '#252525'
|
|
120
|
+
graph.add_color PRRD::Graph::Color.new colortag: 'ARROW', color: '#ff0000'
|
|
121
|
+
|
|
122
|
+
# Set definitions
|
|
123
|
+
|
|
124
|
+
graph.add_definition PRRD::Graph::Definition.new vname: 'ds1', rrdfile: database.path, ds_name: 'ds1', cf: 'AVERAGE'
|
|
125
|
+
graph.add_definition PRRD::Graph::Definition.new vname: 'ds2', rrdfile: database.path, ds_name: 'ds2', cf: 'AVERAGE'
|
|
126
|
+
|
|
127
|
+
# Set lines
|
|
128
|
+
|
|
129
|
+
graph.add_line PRRD::Graph::Line.new value: 'ds1', width: 3, color: PRRD.color(:blue), legend: 'Ds1'
|
|
130
|
+
graph.add_line PRRD::Graph::Line.new value: 'ds2', color: PRRD.color(:blue), legend: 'Ds2'
|
|
131
|
+
|
|
132
|
+
# Create graph
|
|
133
|
+
|
|
134
|
+
graph.generate
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Syntaxes
|
|
139
|
+
|
|
140
|
+
For each element of a database or a graph, there are alternative syntaxes to create them.
|
|
141
|
+
|
|
142
|
+
Let's take an exemple with Datasources, but it is the same for Archives, Colors, Lines, Areas, ... and so on.
|
|
143
|
+
|
|
144
|
+
(Note: each example in the `scripts` folder use a different syntax as a showcase of all different ways to write PRRD scripts)
|
|
145
|
+
|
|
146
|
+
### Object properties
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
ds = PRRD::Database::Datasource.new
|
|
150
|
+
ds.name = 'ds1'
|
|
151
|
+
ds.type = 'GAUGE'
|
|
152
|
+
ds.heartbeat = 600
|
|
153
|
+
ds.min = 0
|
|
154
|
+
ds.max = 'U'
|
|
155
|
+
database.add_datasource ds
|
|
156
|
+
|
|
157
|
+
ds = PRRD::Database::Datasource.new
|
|
158
|
+
ds.name = 'ds2'
|
|
159
|
+
ds.type = 'GAUGE'
|
|
160
|
+
ds.heartbeat = 600
|
|
161
|
+
ds.min = 0
|
|
162
|
+
ds.max = 'U'
|
|
163
|
+
database.add_datasource ds
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Hashes
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
ds = PRRD::Database::Datasource.new name: = 'ds1', type: 'GAUGE', heartbeat: 600, min: 0, max: 'U'
|
|
170
|
+
database.add_datasource ds
|
|
171
|
+
|
|
172
|
+
ds = PRRD::Database::Datasource.new name: = 'ds2', type: 'GAUGE', heartbeat: 600, min: 0, max: 'U'
|
|
173
|
+
database.add_datasource ds
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Array of hashes
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
dss = [
|
|
180
|
+
PRRD::Database::Datasource.new({name: = 'ds1', type: 'GAUGE', heartbeat: 600, min: 0, max: 'U'}),
|
|
181
|
+
PRRD::Database::Datasource.new({name: = 'ds2', type: 'GAUGE', heartbeat: 600, min: 0, max: 'U'}),
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
database.add_datasources dss
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Add objects
|
|
188
|
+
|
|
189
|
+
This syntax auto-detect the type and add it properly to the database or graph object
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
database << PRRD::Database::Datasource.new({name: = 'ds1', type: 'GAUGE', heartbeat: 600, min: 0, max: 'U'})
|
|
193
|
+
graph << PRRD::Graph::Line.new({width: 2, value: = 'ds1', color: PRRD.color(:blue), legend: 'DS1'})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Sample scripts
|
|
197
|
+
|
|
198
|
+
PRRD provides some real-world sample scripts located in the `scripts` directory.
|
|
199
|
+
|
|
200
|
+
- A `memory` script that graphs the consumption of memory and swap on your system
|
|
201
|
+
- A `cpu` script that graphs the usage of your CPU
|
|
202
|
+
- A `process` script that graphs the user and system processes
|
|
203
|
+
- A `network` script that graphs the send and recv packaged on your network interface (eth0)
|
|
204
|
+
|
|
205
|
+
To run them, simply type:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
ruby scripts/memory.rb
|
|
209
|
+
ruby scripts/cpu.rb
|
|
210
|
+
ruby scripts/process.rb
|
|
211
|
+
ruby scripts/network.rb
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Before that, you may be ask to copy `scripts/config.rb-example` to `scripts/config.rb` and edit it to fit to your needs
|
|
215
|
+
|
|
216
|
+
If you want to run them permanently, add these lines to your crontab (adpat *paths* and *username*):
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
*/5 * * * * username ruby /home/username/prrd/scripts/memory.rb > /dev/null
|
|
220
|
+
*/5 * * * * username ruby /home/username/prrd/scripts/cpu.rb > /dev/null
|
|
221
|
+
*/5 * * * * username ruby /home/username/prrd/scripts/process.rb > /dev/null
|
|
222
|
+
*/5 * * * * username ruby /home/username/prrd/scripts/network.rb > /dev/null
|
|
223
|
+
```
|
data/Rakefile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# File: Rakefile
|
|
4
|
+
# Time-stamp: <2014-09-22 22:45:18 pierre>
|
|
5
|
+
# Copyright (C) 2014 Pierre Lecocq
|
|
6
|
+
# Description: Rakefile for PRRD library
|
|
7
|
+
|
|
8
|
+
task :default => :help
|
|
9
|
+
# rake help
|
|
10
|
+
task :help do
|
|
11
|
+
puts 'PRRD - rrdtool ruby interface'
|
|
12
|
+
puts "\n" + 'Available rake tasks:'
|
|
13
|
+
puts ' * test: run rspec tests'
|
|
14
|
+
puts ' * code: run rubocop to verify code quality'
|
|
15
|
+
puts ' * doc: run yard to generate documentation'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# rake test
|
|
19
|
+
require 'rspec/core/rake_task'
|
|
20
|
+
RSpec::Core::RakeTask.new(:test)
|
|
21
|
+
|
|
22
|
+
# rake code
|
|
23
|
+
require 'rubocop/rake_task'
|
|
24
|
+
desc 'Run RuboCop on the lib directory'
|
|
25
|
+
RuboCop::RakeTask.new(:code) do |task|
|
|
26
|
+
task.patterns = ['lib/**/*.rb']
|
|
27
|
+
task.fail_on_error = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# rake doc
|
|
31
|
+
require 'yard'
|
|
32
|
+
YARD::Rake::YardocTask.new do |t|
|
|
33
|
+
t.files = ['lib/**/*.rb']
|
|
34
|
+
end
|
|
35
|
+
task :doc => :yard
|
data/lib/prrd.rb
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# File: prrd.rb
|
|
4
|
+
# Time-stamp: <2014-10-01 21:59:18 pierre>
|
|
5
|
+
# Copyright (C) 2014 Pierre Lecocq
|
|
6
|
+
# Description: RRD ruby module
|
|
7
|
+
|
|
8
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__))
|
|
9
|
+
|
|
10
|
+
# Main PRRD module
|
|
11
|
+
module PRRD
|
|
12
|
+
# Version
|
|
13
|
+
VERSION = [0, 2, 0].join('.')
|
|
14
|
+
|
|
15
|
+
# Class variables
|
|
16
|
+
@@debug_mode = false
|
|
17
|
+
@@colors = nil
|
|
18
|
+
@@bin = nil
|
|
19
|
+
|
|
20
|
+
# Activate debug mode
|
|
21
|
+
def self.activate_debug_mode
|
|
22
|
+
@@debug_mode = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Activate debug mode
|
|
26
|
+
def self.debug_mode
|
|
27
|
+
@@debug_mode
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get rrdtool binary
|
|
31
|
+
# @return [String]
|
|
32
|
+
def self.bin
|
|
33
|
+
if @@bin.nil?
|
|
34
|
+
@@bin = `which rrdtool`.chomp
|
|
35
|
+
fail 'Install rrdtool. See http://oss.oetiker.ch/rrdtool/' if @@bin.nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@@bin
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Execute a command
|
|
42
|
+
# @param cmd [String]
|
|
43
|
+
# @param message [String, Nil]
|
|
44
|
+
# @return [String, Nil]
|
|
45
|
+
def self.execute(cmd, message = nil)
|
|
46
|
+
puts cmd if PRRD.debug_mode
|
|
47
|
+
`#{cmd}`
|
|
48
|
+
|
|
49
|
+
message if $CHILD_STATUS.nil? && !message.nil?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Color class
|
|
53
|
+
class Color
|
|
54
|
+
# Accessors
|
|
55
|
+
attr_accessor :collection, :hexcode
|
|
56
|
+
|
|
57
|
+
# Constructor
|
|
58
|
+
# @param name [Symbol]
|
|
59
|
+
# @param tint [Symbol, nil]
|
|
60
|
+
# @param alpha [Symbol, nil]
|
|
61
|
+
def initialize(name, tint = nil, alpha = nil)
|
|
62
|
+
if name.is_a?(String) && name.include?('#')
|
|
63
|
+
@hexcode = name
|
|
64
|
+
else
|
|
65
|
+
@hexcode = to_hex(name, tint, alpha)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Translate into hexcode
|
|
70
|
+
# @param name [Symbol]
|
|
71
|
+
# @param tint [Symbol, nil]
|
|
72
|
+
# @param alpha [Symbol, nil]
|
|
73
|
+
# @param [String]
|
|
74
|
+
def to_hex(name, tint = nil, alpha = nil)
|
|
75
|
+
if @collection.nil?
|
|
76
|
+
@collection = {
|
|
77
|
+
red: {light: '#EA644A', dark: '#CC3118'},
|
|
78
|
+
orange: {light: '#EC9D48', dark: '#CC7016'},
|
|
79
|
+
yellow: {light: '#ECD748', dark: '#C9B215'},
|
|
80
|
+
green: {light: '#54EC48', dark: '#24BC14'},
|
|
81
|
+
blue: {light: '#48C4EC', dark: '#1598C3'},
|
|
82
|
+
pink: {light: '#DE48EC', dark: '#B415C7'},
|
|
83
|
+
purple: {light: '#7648EC', dark: '#4D18E4'}
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
name = name.to_sym
|
|
88
|
+
tint = tint.nil? ? :light : tint.to_sym
|
|
89
|
+
|
|
90
|
+
fail "Unknown color #{name}" unless @collection.key? name
|
|
91
|
+
fail "Unknown tint #{tint}" unless @collection[name].key? tint
|
|
92
|
+
|
|
93
|
+
alpha.nil? ? @collection[name][tint] : @collection[name][tint] + alpha
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Darken the color
|
|
97
|
+
# @param percentage [Integer]
|
|
98
|
+
# @return [String]
|
|
99
|
+
def darken(percentage)
|
|
100
|
+
fail 'Can not operate on a non-hexadecimal color' if @hexcode.nil?
|
|
101
|
+
|
|
102
|
+
hexcode = @hexcode.gsub('#', '')
|
|
103
|
+
percentage = percentage.to_f / 100
|
|
104
|
+
rgb = hexcode.scan(/../).map { |c| c.hex }
|
|
105
|
+
rgb.map! { |c| (c.to_i * percentage).round }
|
|
106
|
+
|
|
107
|
+
"#%02x%02x%02x" % rgb
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Lighten the color
|
|
111
|
+
# @param percentage [Integer]
|
|
112
|
+
def lighten(percentage)
|
|
113
|
+
fail 'Can not operate on a non-hexadecimal color' if @hexcode.nil?
|
|
114
|
+
|
|
115
|
+
hexcode = @hexcode.gsub('#', '')
|
|
116
|
+
percentage = percentage.to_f / 100
|
|
117
|
+
rgb = hexcode.scan(/../).map { |c| c.hex }
|
|
118
|
+
rgb.map! { |c| [(c.to_i + 255 * percentage).round, 255].min }
|
|
119
|
+
|
|
120
|
+
"#%02x%02x%02x" % rgb
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# String representation
|
|
124
|
+
def to_s
|
|
125
|
+
@hexcode
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Entity base class
|
|
130
|
+
class Entity
|
|
131
|
+
# Accessors
|
|
132
|
+
attr_accessor :data, :keys
|
|
133
|
+
|
|
134
|
+
# Constructor
|
|
135
|
+
def initialize(values = nil)
|
|
136
|
+
@data = {}
|
|
137
|
+
|
|
138
|
+
unless values.nil?
|
|
139
|
+
values.each do |k, v|
|
|
140
|
+
next unless @keys.include? k
|
|
141
|
+
send "#{k}=".to_sym, v
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Validate presence of keys in data collection
|
|
147
|
+
# @param keys [Array]
|
|
148
|
+
def validate_presence(*keys)
|
|
149
|
+
keys.each do |k|
|
|
150
|
+
fail 'Define a "%s" option' % k if !@data.key?(k) || @data[k].nil?
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Method missing
|
|
155
|
+
# @param m [Symbol]
|
|
156
|
+
# @param args [Array]
|
|
157
|
+
# @param block [Proc]
|
|
158
|
+
# @return [Boolean]
|
|
159
|
+
def method_missing(m, *args, &block)
|
|
160
|
+
ms = m.to_s
|
|
161
|
+
if ms.include? '='
|
|
162
|
+
ms = ms[0..-2]
|
|
163
|
+
if @keys.include? ms.to_sym
|
|
164
|
+
@data[ms.to_sym] = args[0]
|
|
165
|
+
return true
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
super
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Requires
|
|
175
|
+
require 'prrd/database'
|
|
176
|
+
require 'prrd/database/datasource'
|
|
177
|
+
require 'prrd/database/archive'
|
|
178
|
+
|
|
179
|
+
require 'prrd/graph'
|
|
180
|
+
require 'prrd/graph/definition'
|
|
181
|
+
require 'prrd/graph/colors'
|
|
182
|
+
require 'prrd/graph/area'
|
|
183
|
+
require 'prrd/graph/line'
|
|
184
|
+
require 'prrd/graph/print'
|
|
185
|
+
require 'prrd/graph/comment'
|
|
186
|
+
require 'prrd/graph/hrule'
|
|
187
|
+
require 'prrd/graph/vrule'
|
|
188
|
+
require 'prrd/graph/shift'
|
|
189
|
+
require 'prrd/graph/textalign'
|