├── .gitignore
├── LICENSE
├── README.md
├── docker-compose.yml
├── frontend
├── Dockerfile
├── containerpilot.json5
├── lib
│ ├── index.js
│ ├── public
│ │ ├── index.html
│ │ ├── js
│ │ │ └── bundle.js
│ │ └── lib
│ │ │ ├── d3.min.js
│ │ │ ├── jquery.js
│ │ │ ├── lodash.min.js
│ │ │ ├── rickshaw.min.css
│ │ │ ├── rickshaw.min.js
│ │ │ └── style.css
│ └── webStream.js
├── memory.sh
└── package.json
├── local-compose.yml
├── project_overview.graffle
├── data.plist
├── image1.pdf
├── image15.tiff
├── image16.tiff
├── image17.tiff
├── image18.tiff
├── image19.tiff
├── image2.png
├── image22.tiff
├── image3.tiff
└── image6.png
├── project_overview.png
├── sensor
├── Dockerfile
├── containerpilot.json5
├── lib
│ └── index.js
└── package.json
├── serializer
├── Dockerfile
├── containerpilot.json5
├── lib
│ └── index.js
├── package.json
└── sensors
│ └── request_count.sh
├── setup.sh
├── smartthings
├── Dockerfile
├── containerpilot.json5
├── health.sh
├── lib
│ └── index.js
├── package.json
└── sensor.groovy
└── traefik
├── Dockerfile
├── containerpilot.json5
└── traefik.toml
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | _env
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node.js Microservices in Containers with ContainerPilot
2 |
3 | Example microservices implementation using Node.js and Docker. Below is an architectural diagram depicting the composition of services that make up the project. When everything is working a frontend web application is accessible that will display a set of graphs using sensor data. The sensor data is either generated by the smartthings microservice or is sent to the microservice from a SmartThings hub running a Sensor SmartApp.
4 |
5 | 
6 |
7 | ## Usage
8 |
9 | ### Development
10 |
11 | In local development you can start the microservices by running
12 |
13 | ```console
14 | $ docker-compose -f local-compose.yml up -d
15 | ```
16 |
17 | Then scale up the frontend by a number of instances (3 in this case):
18 |
19 | ```console
20 | $ docker-compose -f local-compose.yml scale frontend=3
21 | ```
22 |
23 | Navigate to `http://localhost` in your browser and you will see 3 charts. As data flows into the serializer from the various sensors you will start to see data appear on the charts in real-time.
24 |
25 | To check that all of the local containers are running you can execute the `ps` command by running
26 |
27 | ```console
28 | $ docker-compose -f local-compose.yml ps
29 | ```
30 |
31 | ### Production
32 |
33 | When deploying to Triton, first setup your environment then run docker-compose. Below is an example of setting your environment variables then pushing the code to production.
34 |
35 | ```console
36 | $ ./setup.sh
37 | $ eval "$(triton env)"
38 | $ docker-compose up -d
39 | $ triton instance get nodejsexample_traefik_1
40 | ```
41 |
42 |
43 | ## Credits
44 |
45 | This project is inspired by various microservices workshops and trainings. In no particular order, they are:
46 | * https://github.com/lloydbenson/microservices-workshop - Lloyd Benson
47 | * https://github.com/nearform/micro-services-tutorial-iot - nearForm
48 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | consul:
2 | image: autopilotpattern/consul:0.7.2-r0.8
3 | command: >
4 | /usr/local/bin/containerpilot
5 | /bin/consul agent -server
6 | -config-dir=/etc/consul
7 | -log-level=err
8 | -bootstrap-expect 1
9 | -ui-dir /ui
10 | restart: always
11 | labels:
12 | - triton.cns.services=consul
13 | ports:
14 | - "8500:8500"
15 | expose:
16 | - 9090
17 | env_file:
18 | - _env
19 | nats:
20 | image: autopilotpattern/nats:1.0.2-r5
21 | restart: always
22 | expose:
23 | - "9090"
24 | - "4222"
25 | - "8222"
26 | - "6222"
27 | env_file:
28 | - _env
29 | natsboard:
30 | image: d0cker/natsboard:1.0.0
31 | restart: always
32 | ports:
33 | - "3000:3000"
34 | - "3001:3001"
35 | expose:
36 | - "9090"
37 | env_file:
38 | - _env
39 | prometheus:
40 | image: autopilotpattern/prometheus:1.7.1-r24
41 | mem_limit: 128m
42 | restart: always
43 | ports:
44 | - "9090:9090"
45 | env_file:
46 | - _env
47 | influxdb:
48 | image: autopilotpattern/influxdb:1.1.1
49 | restart: always
50 | expose:
51 | - "9090"
52 | - "8086"
53 | - "8083"
54 | env_file:
55 | - _env
56 | environment:
57 | - ADMIN_USER=root
58 | - INFLUXDB_INIT_PWD=root123
59 | - INFLUXDB_ADMIN_ENABLED=true
60 | - INFLUXDB_REPORTING_DISABLED=true
61 | - INFLUXDB_DATA_QUERY_LOG_ENABLED=false
62 | - INFLUXDB_HTTP_LOG_ENABLED=false
63 | - INFLUXDB_CONTINUOUS_QUERIES_LOG_ENABLED=false
64 | traefik:
65 | image: d0cker/traefik:1.3.2
66 | labels:
67 | - triton.cns.services=ui
68 | ports:
69 | - "80:80"
70 | - "8080:8080"
71 | expose:
72 | - "9090"
73 | env_file:
74 | - _env
75 | restart: always
76 | serializer:
77 | image: d0cker/serializer:6.4.0
78 | env_file:
79 | - _env
80 | environment:
81 | - INFLUXDB_USER=root
82 | - INFLUXDB_PWD=root123
83 | expose:
84 | - "80"
85 | - "9090"
86 | restart: always
87 | frontend:
88 | image: d0cker/frontend:6.2.1
89 | env_file:
90 | - _env
91 | expose:
92 | - "80"
93 | - "9090"
94 | restart: always
95 | smartthings:
96 | image: d0cker/smartthings:8.2.1
97 | labels:
98 | - triton.cns.services=smartthings
99 | ports:
100 | - "80"
101 | expose:
102 | - "9090"
103 | env_file:
104 | - _env
105 | environment:
106 | - FAKE_MODE=true
107 | restart: always
108 | humidity:
109 | image: d0cker/sensor:4.3.1
110 | env_file:
111 | - _env
112 | environment:
113 | - SENSOR_TYPE=humidity
114 | expose:
115 | - "9090"
116 | restart: always
117 | motion:
118 | image: d0cker/sensor:4.3.1
119 | env_file:
120 | - _env
121 | environment:
122 | - SENSOR_TYPE=motion
123 | expose:
124 | - "9090"
125 | restart: always
126 | temperature:
127 | image: d0cker/sensor:4.3.1
128 | env_file:
129 | - _env
130 | environment:
131 | - SENSOR_TYPE=temperature
132 | expose:
133 | - "9090"
134 | restart: always
135 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8-alpine
2 |
3 | RUN apk update && \
4 | apk add curl && \
5 | rm -rf /var/cache/apk/*
6 |
7 | RUN export CONSUL_VERSION=0.7.0 \
8 | && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \
9 | && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \
10 | && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \
11 | && unzip /tmp/consul -d /usr/local/bin \
12 | && rm /tmp/consul.zip \
13 | && mkdir /config
14 |
15 | # Install ContainerPilot
16 | ENV CONTAINERPILOT_VERSION 3.4.2
17 | RUN export CP_SHA1=5c99ae9ede01e8fcb9b027b5b3cb0cfd8c0b8b88 \
18 | && curl -Lso /tmp/containerpilot.tar.gz \
19 | "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \
20 | && echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \
21 | && tar zxf /tmp/containerpilot.tar.gz -C /bin \
22 | && rm /tmp/containerpilot.tar.gz
23 |
24 | # COPY ContainerPilot configuration
25 | ENV CONTAINERPILOT_PATH=/etc/containerpilot.json5
26 | COPY containerpilot.json5 ${CONTAINERPILOT_PATH}
27 | ENV CONTAINERPILOT=${CONTAINERPILOT_PATH}
28 |
29 | COPY memory.sh /bin/memory.sh
30 | RUN chmod 755 /bin/memory.sh
31 |
32 | # Install our application
33 | RUN mkdir -p /opt/app/lib/public
34 | COPY package.json /opt/app/
35 | COPY lib/*.js /opt/app/lib/
36 | COPY lib/public /opt/app/lib/public
37 | RUN cd /opt/app && npm install
38 |
39 | CMD ["/bin/containerpilot"]
40 |
--------------------------------------------------------------------------------
/frontend/containerpilot.json5:
--------------------------------------------------------------------------------
1 | {
2 | consul: 'localhost:8500',
3 | jobs: [
4 | {
5 | name: 'frontend',
6 | port: {{.PORT}},
7 | exec: 'node /opt/app/',
8 | health: {
9 | exec: '/usr/bin/curl -o /dev/null --fail -s http://localhost:{{.PORT}}/check-it-out',
10 | interval: 2,
11 | ttl: 5
12 | },
13 | tags: [
14 | 'traefik.backend=frontend',
15 | 'traefik.frontend.rule=Path: /,/lib/{[a-z]+},/js/{[a-z]+}',
16 | 'traefik.frontend.entryPoints=http,ws,wss'
17 | ]
18 | },
19 | {
20 | name: 'consul-agent',
21 | exec: ['/usr/local/bin/consul', 'agent',
22 | '-data-dir=/data',
23 | '-config-dir=/config',
24 | '-log-level=err',
25 | '-rejoin',
26 | '-retry-join', '{{ .CONSUL | default "consul" }}',
27 | '-retry-max', '10',
28 | '-retry-interval', '10s'],
29 | restarts: 'unlimited'
30 | },
31 | {
32 | name: 'onchange-serializer',
33 | exec: 'pkill -SIGHUP node',
34 | when: {
35 | source: 'watch.serializer',
36 | each: 'changed'
37 | }
38 | },
39 | {
40 | name: 'memory-sensor',
41 | exec: '/bin/memory.sh',
42 | timeout: '5s',
43 | when: {
44 | interval: '5s'
45 | },
46 | restarts: 'unlimited'
47 | }
48 | ],
49 | watches: [
50 | {
51 | name: 'serializer',
52 | interval: 3
53 | }
54 | ],
55 | telemetry: {
56 | port: 9090,
57 | tags: ['op'],
58 | interfaces: ['eth1', 'eth0', 'eth0[1]', 'lo', 'lo0', 'inet'],
59 | metrics: [
60 | {
61 | namespace: 'example',
62 | subsystem: 'frontend',
63 | name: 'free_memory',
64 | help: 'Amount of free memory',
65 | type: 'gauge'
66 | }
67 | ]
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/frontend/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Load modules
4 |
5 | const Path = require('path');
6 | const Brule = require('brule');
7 | const Hapi = require('hapi');
8 | const Inert = require('inert');
9 | const Piloted = require('piloted');
10 | const WebStream = require('./webStream');
11 | const Wreck = require('wreck');
12 |
13 |
14 |
15 | function main () {
16 | const serverConfig = {
17 | connections: {
18 | routes: {
19 | files: {
20 | relativeTo: Path.join(__dirname, 'public')
21 | }
22 | }
23 | },
24 | load: {
25 | sampleInterval: 100
26 | }
27 | };
28 |
29 | const server = new Hapi.Server(serverConfig);
30 | server.connection({
31 | port: process.env.PORT,
32 | load: {
33 | maxEventLoopDelay: 30 // 30 milliseconds
34 | }
35 | });
36 | server.register([Inert, Brule], () => {
37 | server.route({
38 | method: 'GET',
39 | path: '/{param*}',
40 | handler: {
41 | directory: {
42 | path: '.',
43 | redirectToSlash: true,
44 | index: true
45 | }
46 | }
47 | });
48 |
49 | server.start(() => {
50 | console.log(`listening at http://localhost:${server.info.port}`);
51 | startReading(WebStream(server.listener));
52 | });
53 | });
54 | }
55 | main();
56 |
57 | function startReading (webStream) {
58 | let lastEmitted = 0;
59 | setInterval(() => {
60 | const serializer = Piloted.service('serializer');
61 | if (!serializer) {
62 | console.log('Serializer not found');
63 | return;
64 | }
65 |
66 | Wreck.get(`http://${serializer.address}:${serializer.port}/read`, { json: 'force' }, (err, res, points) => {
67 | if (err) {
68 | console.error('Error making request to serializer: ' + err);
69 | return;
70 | }
71 |
72 | if (!points || !points.length) {
73 | return;
74 | }
75 |
76 | let toEmit = [];
77 | points.forEach((point) => {
78 | if (!point) {
79 | return;
80 | }
81 | point.time = (new Date(point.time || Date.now())).getTime();
82 |
83 | if (point.time > lastEmitted) {
84 | lastEmitted = point.time;
85 | toEmit.push(point);
86 | }
87 | });
88 |
89 | if (toEmit.length) {
90 | webStream.emit([].concat.apply([], toEmit));
91 | }
92 | });
93 | }, 1000);
94 | }
95 |
--------------------------------------------------------------------------------
/frontend/lib/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Charting frontend
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
Temperature Sensor
16 |
17 |
21 |
22 |
23 |
Humidity Sensor
24 |
25 |
29 |
30 |
31 |
Motion Sensor
32 |
33 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/frontend/lib/public/lib/lodash.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * lodash 3.10.1 (Custom Build) lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE
4 | * Build: `lodash modern -o ./lodash.js`
5 | */
6 | ;(function(){function n(n,t){if(n!==t){var r=null===n,e=n===w,u=n===n,o=null===t,i=t===w,f=t===t;if(n>t&&!o||!u||r&&!i&&f||e&&f)return 1;if(n=n&&9<=n&&13>=n||32==n||160==n||5760==n||6158==n||8192<=n&&(8202>=n||8232==n||8233==n||8239==n||8287==n||12288==n||65279==n);
8 | }function v(n,t){for(var r=-1,e=n.length,u=-1,o=[];++r=F&&gu&&lu?new Dn(t):null,c=t.length;a&&(i=Mn,f=false,t=a);n:for(;++oi(t,a,0)&&u.push(a);return u}function at(n,t){var r=true;return Su(n,function(n,e,u){return r=!!t(n,e,u)}),r}function ct(n,t,r,e){var u=e,o=u;return Su(n,function(n,i,f){i=+t(n,i,f),(r(i,u)||i===e&&i===o)&&(u=i,
14 | o=n)}),o}function lt(n,t){var r=[];return Su(n,function(n,e,u){t(n,e,u)&&r.push(n)}),r}function st(n,t,r,e){var u;return r(n,function(n,r,o){return t(n,r,o)?(u=e?r:n,false):void 0}),u}function pt(n,t,r,e){e||(e=[]);for(var u=-1,o=n.length;++ut&&(t=-t>u?0:u+t),r=r===w||r>u?u:+r||0,0>r&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0,r=Be(u);++e=c)break n;o=e[o],u*="asc"===o||true===o?1:-1;break n}u=t.b-r.b}return u})}function $t(n,t){
21 | var r=0;return Su(n,function(n,e,u){r+=+t(n,e,u)||0}),r}function St(n,t){var e=-1,u=xr(),o=n.length,i=u===r,f=i&&o>=F,a=f&&gu&&lu?new Dn(void 0):null,c=[];a?(u=Mn,i=false):(f=false,a=t?[]:c);n:for(;++eu(a,s,0)&&((t||f)&&a.push(s),c.push(l))}return c}function Ft(n,t){for(var r=-1,e=t.length,u=Be(e);++r>>1,i=n[o];(r?i<=t:iu?w:o,u=1);++e=F)return t.plant(e).value();for(var u=0,n=r?o[u].apply(this,n):e;++uarguments.length;return typeof e=="function"&&o===w&&Oo(r)?n(r,e,u,i):Ot(r,wr(e,o,4),u,i,t)}}function sr(n,t,r,e,u,o,i,f,a,c){function l(){for(var m=arguments.length,b=m,j=Be(m);b--;)j[b]=arguments[b];if(e&&(j=Mt(j,e,u)),o&&(j=qt(j,o,i)),_||y){var b=l.placeholder,k=v(j,b),m=m-k.length;if(mt?0:t)):[]}function Pr(n,t,r){var e=n?n.length:0;return e?((r?Ur(n,t,r):null==t)&&(t=1),t=e-(+t||0),Et(n,0,0>t?0:t)):[]}function Kr(n){return n?n[0]:w}function Vr(n,t,e){var u=n?n.length:0;if(!u)return-1;if(typeof e=="number")e=0>e?bu(u+e,0):e;else if(e)return e=Lt(n,t),
42 | er?bu(u+r,0):r||0,typeof n=="string"||!Oo(n)&&be(n)?r<=u&&-1t?0:+t||0,e);++r=n&&(t=w),r}}function ae(n,t,r){function e(t,r){r&&iu(r),a=p=h=w,t&&(_=ho(),c=n.apply(s,f),p||a||(f=s=w))}function u(){var n=t-(ho()-l);0>=n||n>t?e(h,a):p=su(u,n)}function o(){e(g,p);
46 | }function i(){if(f=arguments,l=ho(),s=this,h=g&&(p||!y),false===v)var r=y&&!p;else{a||y||(_=l);var e=v-(l-_),i=0>=e||e>v;i?(a&&(a=iu(a)),_=l,c=n.apply(s,f)):a||(a=su(o,e))}return i&&p?p=iu(p):p||t===v||(p=su(u,t)),r&&(i=true,c=n.apply(s,f)),!i||p||a||(f=s=w),c}var f,a,c,l,s,p,h,_=0,v=false,g=true;if(typeof n!="function")throw new Ge(L);if(t=0>t?0:+t||0,true===r)var y=true,g=false;else ge(r)&&(y=!!r.leading,v="maxWait"in r&&bu(+r.maxWait||0,t),g="trailing"in r?!!r.trailing:g);return i.cancel=function(){p&&iu(p),a&&iu(a),
47 | _=0,a=p=h=w},i}function ce(n,t){function r(){var e=arguments,u=t?t.apply(this,e):e[0],o=r.cache;return o.has(u)?o.get(u):(e=n.apply(this,e),r.cache=o.set(u,e),e)}if(typeof n!="function"||t&&typeof t!="function")throw new Ge(L);return r.cache=new ce.Cache,r}function le(n,t){if(typeof n!="function")throw new Ge(L);return t=bu(t===w?n.length-1:+t||0,0),function(){for(var r=arguments,e=-1,u=bu(r.length-t,0),o=Be(u);++et}function pe(n){return h(n)&&Er(n)&&nu.call(n,"callee")&&!cu.call(n,"callee")}function he(n,t,r,e){return e=(r=typeof r=="function"?Bt(r,e,3):w)?r(n,t):w,e===w?dt(n,t,r):!!e}function _e(n){return h(n)&&typeof n.message=="string"&&ru.call(n)==P}function ve(n){return ge(n)&&ru.call(n)==K}function ge(n){var t=typeof n;return!!n&&("object"==t||"function"==t)}function ye(n){
49 | return null==n?false:ve(n)?uu.test(Qe.call(n)):h(n)&&Rn.test(n)}function de(n){return typeof n=="number"||h(n)&&ru.call(n)==V}function me(n){var t;if(!h(n)||ru.call(n)!=Z||pe(n)||!(nu.call(n,"constructor")||(t=n.constructor,typeof t!="function"||t instanceof t)))return false;var r;return ht(n,function(n,t){r=t}),r===w||nu.call(n,r)}function we(n){return ge(n)&&ru.call(n)==Y}function be(n){return typeof n=="string"||h(n)&&ru.call(n)==G}function xe(n){return h(n)&&Sr(n.length)&&!!Sn[ru.call(n)]}function Ae(n,t){
50 | return nt||!n||!mu(t))return r;do t%2&&(r+=n),t=yu(t/2),n+=n;while(t);return r}function We(n,t,r){var e=n;return(n=u(n))?(r?Ur(e,t,r):null==t)?n.slice(g(n),y(n)+1):(t+="",n.slice(o(n,t),i(n,t)+1)):n}function $e(n,t,r){return r&&Ur(n,t,r)&&(t=w),n=u(n),n.match(t||Wn)||[]}function Se(n,t,r){return r&&Ur(n,t,r)&&(t=w),h(n)?Ne(n):ut(n,t)}function Fe(n){
52 | return n}function Ne(n){return bt(ot(n,true))}function Te(n,t,r){if(null==r){var e=ge(t),u=e?zo(t):w;((u=u&&u.length?gt(t,u):w)?u.length:e)||(u=false,r=t,t=n,n=this)}u||(u=gt(t,zo(t)));var o=true,e=-1,i=ve(n),f=u.length;false===r?o=false:ge(r)&&"chain"in r&&(o=r.chain);for(;++e=$)return r}else n=0;return Lu(r,e)}}(),Mu=le(function(n,t){
55 | return h(n)&&Er(n)?ft(n,pt(t,false,true)):[]}),qu=tr(),Pu=tr(true),Ku=le(function(n){for(var t=n.length,e=t,u=Be(l),o=xr(),i=o===r,f=[];e--;){var a=n[e]=Er(a=n[e])?a:[];u[e]=i&&120<=a.length&&gu&&lu?new Dn(e&&a):null}var i=n[0],c=-1,l=i?i.length:0,s=u[0];n:for(;++c(s?Mn(s,a):o(f,a,0))){for(e=t;--e;){var p=u[e];if(0>(p?Mn(p,a):o(n[e],a,0)))continue n}s&&s.push(a),f.push(a)}return f}),Vu=le(function(t,r){r=pt(r);var e=rt(t,r);return It(t,r.sort(n)),e}),Zu=vr(),Yu=vr(true),Gu=le(function(n){return St(pt(n,false,true));
56 | }),Ju=le(function(n,t){return Er(n)?ft(n,t):[]}),Xu=le(Jr),Hu=le(function(n){var t=n.length,r=2--n?t.apply(this,arguments):void 0}},Nn.ary=function(n,t,r){return r&&Ur(n,t,r)&&(t=w),t=n&&null==t?n.length:bu(+t||0,0),gr(n,E,w,w,w,w,t)},Nn.assign=Co,Nn.at=no,Nn.before=fe,Nn.bind=_o,Nn.bindAll=vo,Nn.bindKey=go,Nn.callback=Se,Nn.chain=Qr,Nn.chunk=function(n,t,r){t=(r?Ur(n,t,r):null==t)?1:bu(yu(t)||1,1),r=0;for(var e=n?n.length:0,u=-1,o=Be(vu(e/t));rr&&(r=-r>u?0:u+r),e=e===w||e>u?u:+e||0,0>e&&(e+=u),u=r>e?0:e>>>0,r>>>=0;rt?0:t)):[]},Nn.takeRight=function(n,t,r){var e=n?n.length:0;return e?((r?Ur(n,t,r):null==t)&&(t=1),t=e-(+t||0),Et(n,0>t?0:t)):[]},Nn.takeRightWhile=function(n,t,r){
71 | return n&&n.length?Nt(n,wr(t,r,3),false,true):[]},Nn.takeWhile=function(n,t,r){return n&&n.length?Nt(n,wr(t,r,3)):[]},Nn.tap=function(n,t,r){return t.call(r,n),n},Nn.throttle=function(n,t,r){var e=true,u=true;if(typeof n!="function")throw new Ge(L);return false===r?e=false:ge(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),ae(n,t,{leading:e,maxWait:+t,trailing:u})},Nn.thru=ne,Nn.times=function(n,t,r){if(n=yu(n),1>n||!mu(n))return[];var e=-1,u=Be(xu(n,4294967295));for(t=Bt(t,r,1);++ee?u[e]=t(e):t(e);
72 | return u},Nn.toArray=je,Nn.toPlainObject=ke,Nn.transform=function(n,t,r,e){var u=Oo(n)||xe(n);return t=wr(t,e,4),null==r&&(u||ge(n)?(e=n.constructor,r=u?Oo(n)?new e:[]:$u(ve(e)?e.prototype:w)):r={}),(u?Pn:_t)(n,function(n,e,u){return t(r,n,e,u)}),r},Nn.union=Gu,Nn.uniq=Gr,Nn.unzip=Jr,Nn.unzipWith=Xr,Nn.values=Ee,Nn.valuesIn=function(n){return Ft(n,Re(n))},Nn.where=function(n,t){return re(n,bt(t))},Nn.without=Ju,Nn.wrap=function(n,t){return t=null==t?Fe:t,gr(t,R,w,[n],[])},Nn.xor=function(){for(var n=-1,t=arguments.length;++nr?0:+r||0,e),r-=t.length,0<=r&&n.indexOf(t,r)==r},Nn.escape=function(n){return(n=u(n))&&hn.test(n)?n.replace(sn,c):n},Nn.escapeRegExp=function(n){return(n=u(n))&&bn.test(n)?n.replace(wn,l):n||"(?:)"},Nn.every=te,Nn.find=ro,Nn.findIndex=qu,Nn.findKey=$o,Nn.findLast=eo,
75 | Nn.findLastIndex=Pu,Nn.findLastKey=So,Nn.findWhere=function(n,t){return ro(n,bt(t))},Nn.first=Kr,Nn.floor=ni,Nn.get=function(n,t,r){return n=null==n?w:yt(n,Dr(t),t+""),n===w?r:n},Nn.gt=se,Nn.gte=function(n,t){return n>=t},Nn.has=function(n,t){if(null==n)return false;var r=nu.call(n,t);if(!r&&!Wr(t)){if(t=Dr(t),n=1==t.length?n:yt(n,Et(t,0,-1)),null==n)return false;t=Zr(t),r=nu.call(n,t)}return r||Sr(n.length)&&Cr(t,n.length)&&(Oo(n)||pe(n))},Nn.identity=Fe,Nn.includes=ee,Nn.indexOf=Vr,Nn.inRange=function(n,t,r){
76 | return t=+t||0,r===w?(r=t,t=0):r=+r||0,n>=xu(t,r)&&nr?bu(e+r,0):xu(r||0,e-1))+1;else if(r)return u=Lt(n,t,true)-1,n=n[u],(t===t?t===n:n!==n)?u:-1;
78 | if(t!==t)return p(n,u,true);for(;u--;)if(n[u]===t)return u;return-1},Nn.lt=Ae,Nn.lte=function(n,t){return n<=t},Nn.max=ti,Nn.min=ri,Nn.noConflict=function(){return Zn._=eu,this},Nn.noop=Le,Nn.now=ho,Nn.pad=function(n,t,r){n=u(n),t=+t;var e=n.length;return er?0:+r||0,n.length),n.lastIndexOf(t,r)==r},Nn.sum=function(n,t,r){if(r&&Ur(n,t,r)&&(t=w),t=wr(t,r,3),1==t.length){n=Oo(n)?n:zr(n),r=n.length;for(var e=0;r--;)e+=+t(n[r])||0;n=e}else n=$t(n,t);return n},Nn.template=function(n,t,r){var e=Nn.templateSettings;r&&Ur(n,t,r)&&(t=r=w),n=u(n),t=nt(tt({},r||t),e,Qn),r=nt(tt({},t.imports),e.imports,Qn);
81 | var o,i,f=zo(r),a=Ft(r,f),c=0;r=t.interpolate||Cn;var l="__p+='";r=Ze((t.escape||Cn).source+"|"+r.source+"|"+(r===gn?jn:Cn).source+"|"+(t.evaluate||Cn).source+"|$","g");var p="sourceURL"in t?"//# sourceURL="+t.sourceURL+"\n":"";if(n.replace(r,function(t,r,e,u,f,a){return e||(e=u),l+=n.slice(c,a).replace(Un,s),r&&(o=true,l+="'+__e("+r+")+'"),f&&(i=true,l+="';"+f+";\n__p+='"),e&&(l+="'+((__t=("+e+"))==null?'':__t)+'"),c=a+t.length,t}),l+="';",(t=t.variable)||(l="with(obj){"+l+"}"),l=(i?l.replace(fn,""):l).replace(an,"$1").replace(cn,"$1;"),
82 | l="function("+(t||"obj")+"){"+(t?"":"obj||(obj={});")+"var __t,__p=''"+(o?",__e=_.escape":"")+(i?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+l+"return __p}",t=Jo(function(){return qe(f,p+"return "+l).apply(w,a)}),t.source=l,_e(t))throw t;return t},Nn.trim=We,Nn.trimLeft=function(n,t,r){var e=n;return(n=u(n))?n.slice((r?Ur(e,t,r):null==t)?g(n):o(n,t+"")):n},Nn.trimRight=function(n,t,r){var e=n;return(n=u(n))?(r?Ur(e,t,r):null==t)?n.slice(0,y(n)+1):n.slice(0,i(n,t+"")+1):n;
83 | },Nn.trunc=function(n,t,r){r&&Ur(n,t,r)&&(t=w);var e=U;if(r=W,null!=t)if(ge(t)){var o="separator"in t?t.separator:o,e="length"in t?+t.length||0:e;r="omission"in t?u(t.omission):r}else e=+t||0;if(n=u(n),e>=n.length)return n;if(e-=r.length,1>e)return r;if(t=n.slice(0,e),null==o)return t+r;if(we(o)){if(n.slice(e).search(o)){var i,f=n.slice(0,e);for(o.global||(o=Ze(o.source,(kn.exec(o)||"")+"g")),o.lastIndex=0;n=o.exec(f);)i=n.index;t=t.slice(0,null==i?e:i)}}else n.indexOf(o,e)!=e&&(o=t.lastIndexOf(o),
84 | -1u.__dir__?"Right":"")}),u},zn.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()}}),Pn(["filter","map","takeWhile"],function(n,t){
86 | var r=t+1,e=r!=T;zn.prototype[n]=function(n,t){var u=this.clone();return u.__iteratees__.push({iteratee:wr(n,t,1),type:r}),u.__filtered__=u.__filtered__||e,u}}),Pn(["first","last"],function(n,t){var r="take"+(t?"Right":"");zn.prototype[n]=function(){return this[r](1).value()[0]}}),Pn(["initial","rest"],function(n,t){var r="drop"+(t?"":"Right");zn.prototype[n]=function(){return this.__filtered__?new zn(this):this[r](1)}}),Pn(["pluck","where"],function(n,t){var r=t?"filter":"map",e=t?bt:ze;zn.prototype[n]=function(n){
87 | return this[r](e(n))}}),zn.prototype.compact=function(){return this.filter(Fe)},zn.prototype.reject=function(n,t){return n=wr(n,t,1),this.filter(function(t){return!n(t)})},zn.prototype.slice=function(n,t){n=null==n?0:+n||0;var r=this;return r.__filtered__&&(0t)?new zn(r):(0>n?r=r.takeRight(-n):n&&(r=r.drop(n)),t!==w&&(t=+t||0,r=0>t?r.dropRight(-t):r.take(t-n)),r)},zn.prototype.takeRightWhile=function(n,t){return this.reverse().takeWhile(n,t).reverse()},zn.prototype.toArray=function(){return this.take(Ru);
88 | },_t(zn.prototype,function(n,t){var r=/^(?:filter|map|reject)|While$/.test(t),e=/^(?:first|last)$/.test(t),u=Nn[e?"take"+("last"==t?"Right":""):t];u&&(Nn.prototype[t]=function(){function t(n){return e&&i?u(n,1)[0]:u.apply(w,Jn([n],o))}var o=e?[1]:arguments,i=this.__chain__,f=this.__wrapped__,a=!!this.__actions__.length,c=f instanceof zn,l=o[0],s=c||Oo(f);return s&&r&&typeof l=="function"&&1!=l.length&&(c=s=false),l={func:ne,args:[t],thisArg:w},a=c&&!a,e&&!i?a?(f=f.clone(),f.__actions__.push(l),n.call(f)):u.call(w,this.value())[0]:!e&&s?(f=a?f:new zn(this),
89 | f=n.apply(f,o),f.__actions__.push(l),new Ln(f,i)):this.thru(t)})}),Pn("join pop push replace shift sort splice split unshift".split(" "),function(n){var t=(/^(?:replace|split)$/.test(n)?He:Je)[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:join|pop|replace|shift)$/.test(n);Nn.prototype[n]=function(){var n=arguments;return e&&!this.__chain__?t.apply(this.value(),n):this[r](function(r){return t.apply(r,n)})}}),_t(zn.prototype,function(n,t){var r=Nn[t];if(r){var e=r.name+"";(Wu[e]||(Wu[e]=[])).push({
90 | name:t,func:r})}}),Wu[sr(w,A).name]=[{name:"wrapper",func:w}],zn.prototype.clone=function(){var n=new zn(this.__wrapped__);return n.__actions__=qn(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=qn(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=qn(this.__views__),n},zn.prototype.reverse=function(){if(this.__filtered__){var n=new zn(this);n.__dir__=-1,n.__filtered__=true}else n=this.clone(),n.__dir__*=-1;return n},zn.prototype.value=function(){
91 | var n,t=this.__wrapped__.value(),r=this.__dir__,e=Oo(t),u=0>r,o=e?t.length:0;n=o;for(var i=this.__views__,f=0,a=-1,c=i.length;++a"'`]/g,pn=RegExp(ln.source),hn=RegExp(sn.source),_n=/<%-([\s\S]+?)%>/g,vn=/<%([\s\S]+?)%>/g,gn=/<%=([\s\S]+?)%>/g,yn=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\n\\]|\\.)*?\1)\]/,dn=/^\w*$/,mn=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\n\\]|\\.)*?)\2)\]/g,wn=/^[:!,]|[\\^$.*+?()[\]{}|\/]|(^[0-9a-fA-Fnrtuvx])|([\n\r\u2028\u2029])/g,bn=RegExp(wn.source),xn=/[\u0300-\u036f\ufe20-\ufe23]/g,An=/\\(\\)?/g,jn=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,kn=/\w*$/,In=/^0[xX]/,Rn=/^\[object .+?Constructor\]$/,On=/^\d+$/,En=/[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g,Cn=/($^)/,Un=/['\n\r\u2028\u2029\\]/g,Wn=RegExp("[A-Z\\xc0-\\xd6\\xd8-\\xde]+(?=[A-Z\\xc0-\\xd6\\xd8-\\xde][a-z\\xdf-\\xf6\\xf8-\\xff]+)|[A-Z\\xc0-\\xd6\\xd8-\\xde]?[a-z\\xdf-\\xf6\\xf8-\\xff]+|[A-Z\\xc0-\\xd6\\xd8-\\xde]+|[0-9]+","g"),$n="Array ArrayBuffer Date Error Float32Array Float64Array Function Int8Array Int16Array Int32Array Math Number Object RegExp Set String _ clearTimeout isFinite parseFloat parseInt setTimeout TypeError Uint8Array Uint8ClampedArray Uint16Array Uint32Array WeakMap".split(" "),Sn={};
94 | Sn[X]=Sn[H]=Sn[Q]=Sn[nn]=Sn[tn]=Sn[rn]=Sn[en]=Sn[un]=Sn[on]=true,Sn[B]=Sn[D]=Sn[J]=Sn[M]=Sn[q]=Sn[P]=Sn[K]=Sn["[object Map]"]=Sn[V]=Sn[Z]=Sn[Y]=Sn["[object Set]"]=Sn[G]=Sn["[object WeakMap]"]=false;var Fn={};Fn[B]=Fn[D]=Fn[J]=Fn[M]=Fn[q]=Fn[X]=Fn[H]=Fn[Q]=Fn[nn]=Fn[tn]=Fn[V]=Fn[Z]=Fn[Y]=Fn[G]=Fn[rn]=Fn[en]=Fn[un]=Fn[on]=true,Fn[P]=Fn[K]=Fn["[object Map]"]=Fn["[object Set]"]=Fn["[object WeakMap]"]=false;var Nn={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a",
95 | "\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y",
96 | "\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss"},Tn={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},Ln={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},zn={"function":true,object:true},Bn={0:"x30",1:"x31",2:"x32",3:"x33",4:"x34",5:"x35",6:"x36",7:"x37",8:"x38",9:"x39",A:"x41",B:"x42",C:"x43",D:"x44",E:"x45",F:"x46",a:"x61",b:"x62",c:"x63",d:"x64",e:"x65",f:"x66",n:"x6e",r:"x72",t:"x74",u:"x75",v:"x76",x:"x78"},Dn={"\\":"\\",
97 | "'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Mn=zn[typeof exports]&&exports&&!exports.nodeType&&exports,qn=zn[typeof module]&&module&&!module.nodeType&&module,Pn=zn[typeof self]&&self&&self.Object&&self,Kn=zn[typeof window]&&window&&window.Object&&window,Vn=qn&&qn.exports===Mn&&Mn,Zn=Mn&&qn&&typeof global=="object"&&global&&global.Object&&global||Kn!==(this&&this.window)&&Kn||Pn||this,Yn=m();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Zn._=Yn, define(function(){
98 | return Yn})):Mn&&qn?Vn?(qn.exports=Yn)._=Yn:Mn._=Yn:Zn._=Yn}).call(this);
--------------------------------------------------------------------------------
/frontend/lib/public/lib/rickshaw.min.css:
--------------------------------------------------------------------------------
1 | .rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px}
2 |
--------------------------------------------------------------------------------
/frontend/lib/public/lib/rickshaw.min.js:
--------------------------------------------------------------------------------
1 | (function(root,factory){if(typeof define==="function"&&define.amd){define(["d3"],function(d3){return root.Rickshaw=factory(d3)})}else if(typeof exports==="object"){module.exports=factory(require("d3"))}else{root.Rickshaw=factory(d3)}})(this,function(d3){var Rickshaw={namespace:function(namespace,obj){var parts=namespace.split(".");var parent=Rickshaw;for(var i=1,length=parts.length;i0){var x=s.data[0].x;var y=s.data[0].y;if(typeof x!="number"||typeof y!="number"&&y!==null){throw"x and y properties of points should be numbers instead of "+typeof x+" and "+typeof y}}if(s.data.length>=3){if(s.data[2].xthis.window.xMax)isInRange=false;return isInRange}return true};this.onUpdate=function(callback){this.updateCallbacks.push(callback)};this.onConfigure=function(callback){this.configureCallbacks.push(callback)};this.registerRenderer=function(renderer){this._renderers=this._renderers||{};this._renderers[renderer.name]=renderer};this.configure=function(args){this.config=this.config||{};if(args.width||args.height){this.setSize(args)}Rickshaw.keys(this.defaults).forEach(function(k){this.config[k]=k in args?args[k]:k in this?this[k]:this.defaults[k]},this);Rickshaw.keys(this.config).forEach(function(k){this[k]=this.config[k]},this);if("stack"in args)args.unstack=!args.stack;var renderer=args.renderer||this.renderer&&this.renderer.name||"stack";this.setRenderer(renderer,args);this.configureCallbacks.forEach(function(callback){callback(args)})};this.setRenderer=function(r,args){if(typeof r=="function"){this.renderer=new r({graph:self});this.registerRenderer(this.renderer)}else{if(!this._renderers[r]){throw"couldn't find renderer "+r}this.renderer=this._renderers[r]}if(typeof args=="object"){this.renderer.configure(args)}};this.setSize=function(args){args=args||{};if(typeof window!==undefined){var style=window.getComputedStyle(this.element,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);var elementHeight=parseInt(style.getPropertyValue("height"),10)}this.width=args.width||elementWidth||400;this.height=args.height||elementHeight||250;this.vis&&this.vis.attr("width",this.width).attr("height",this.height)};this.initialize(args)};Rickshaw.namespace("Rickshaw.Fixtures.Color");Rickshaw.Fixtures.Color=function(){this.schemes={};this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse();this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"];this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"];this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse();this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"};this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse();this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"];this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]};Rickshaw.namespace("Rickshaw.Fixtures.RandomData");Rickshaw.Fixtures.RandomData=function(timeInterval){var addData;timeInterval=timeInterval||1;var lastRandomValue=200;var timeBase=Math.floor((new Date).getTime()/1e3);this.addData=function(data){var randomValue=Math.random()*100+15+lastRandomValue;var index=data[0].length;var counter=1;data.forEach(function(series){var randomVariance=Math.random()*20;var v=randomValue/25+counter++ +(Math.cos(index*counter*11/960)+2)*15+(Math.cos(index/7)+2)*7+(Math.cos(index/17)+2)*1;series.push({x:index*timeInterval+timeBase,y:v+randomVariance})});lastRandomValue=randomValue*.85};this.removeData=function(data){data.forEach(function(series){series.shift()});timeBase+=timeInterval}};Rickshaw.namespace("Rickshaw.Fixtures.Time");Rickshaw.Fixtures.Time=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getUTCFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getUTCFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getUTCMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getUTCDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getUTCMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getUTCMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toUTCString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="month"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),date.getUTCMonth())/1e3;if(floor==time)return time;year=date.getUTCFullYear();var month=date.getUTCMonth();if(month==11){month=0;year=year+1}else{month+=1}return Date.UTC(year,month)/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),0)/1e3;if(floor==time)return time;year=date.getUTCFullYear()+1;return Date.UTC(year,0)/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Time.Local");Rickshaw.Fixtures.Time.Local=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="day"){var nearFuture=new Date((time+unit.seconds-1)*1e3);var rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(nearFuture.getDate());rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="month"){date=new Date(time*1e3);floor=new Date(date.getFullYear(),date.getMonth()).getTime()/1e3;if(floor==time)return time;year=date.getFullYear();var month=date.getMonth();if(month==11){month=0;year=year+1}else{month+=1}return new Date(year,month).getTime()/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=new Date(date.getUTCFullYear(),0).getTime()/1e3;if(floor==time)return time;year=date.getFullYear()+1;return new Date(year,0).getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Number");Rickshaw.Fixtures.Number.formatKMBT=function(y){var abs_y=Math.abs(y);if(abs_y>=1e12){return y/1e12+"T"}else if(abs_y>=1e9){return y/1e9+"B"}else if(abs_y>=1e6){return y/1e6+"M"}else if(abs_y>=1e3){return y/1e3+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(y){var abs_y=Math.abs(y);if(abs_y>=0x4000000000000){return y/0x4000000000000+"P"}else if(abs_y>=1099511627776){return y/1099511627776+"T"}else if(abs_y>=1073741824){return y/1073741824+"G"}else if(abs_y>=1048576){return y/1048576+"M"}else if(abs_y>=1024){return y/1024+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.namespace("Rickshaw.Color.Palette");Rickshaw.Color.Palette=function(args){var color=new Rickshaw.Fixtures.Color;args=args||{};this.schemes={};this.scheme=color.schemes[args.scheme]||args.scheme||color.schemes.colorwheel;this.runningIndex=0;this.generatorIndex=0;if(args.interpolatedStopCount){var schemeCount=this.scheme.length-1;var i,j,scheme=[];for(i=0;iself.graph.x.range()[1]){if(annotation.element){annotation.line.classList.add("offscreen");annotation.element.style.display="none"}annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.add("offscreen")});return}if(!annotation.element){var element=annotation.element=document.createElement("div");element.classList.add("annotation");this.elements.timeline.appendChild(element);element.addEventListener("click",function(e){element.classList.toggle("active");annotation.line.classList.toggle("active");annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.toggle("active")})},false)}annotation.element.style.left=left+"px";annotation.element.style.display="block";annotation.boxes.forEach(function(box){var element=box.element;if(!element){element=box.element=document.createElement("div");element.classList.add("content");element.innerHTML=box.content;annotation.element.appendChild(element);annotation.line=document.createElement("div");annotation.line.classList.add("annotation_line");self.graph.element.appendChild(annotation.line);if(box.end){box.rangeElement=document.createElement("div");box.rangeElement.classList.add("annotation_range");self.graph.element.appendChild(box.rangeElement)}}if(box.end){var annotationRangeStart=left;var annotationRangeEnd=Math.min(self.graph.x(box.end),self.graph.x.range()[1]);if(annotationRangeStart>annotationRangeEnd){annotationRangeEnd=left;annotationRangeStart=Math.max(self.graph.x(box.end),self.graph.x.range()[0])}var annotationRangeWidth=annotationRangeEnd-annotationRangeStart;box.rangeElement.style.left=annotationRangeStart+"px";box.rangeElement.style.width=annotationRangeWidth+"px";box.rangeElement.classList.remove("offscreen")}annotation.line.classList.remove("offscreen");annotation.line.style.left=left+"px"})},this)};this.graph.onUpdate(function(){self.update()})};Rickshaw.namespace("Rickshaw.Graph.Axis.Time");Rickshaw.Graph.Axis.Time=function(args){var self=this;this.graph=args.graph;this.elements=[];this.ticksTreatment=args.ticksTreatment||"plain";this.fixedTimeUnit=args.timeUnit;var time=args.timeFixture||new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var unit;var units=time.units;var domain=this.graph.x.domain();var rangeSeconds=domain[1]-domain[0];units.forEach(function(u){if(Math.floor(rangeSeconds/u.seconds)>=2){unit=unit||u}});return unit||time.units[time.units.length-1]};this.tickOffsets=function(){var domain=this.graph.x.domain();var unit=this.fixedTimeUnit||this.appropriateTimeUnit();var count=Math.ceil((domain[1]-domain[0])/unit.seconds);var runningTick=domain[0];var offsets=[];for(var i=0;iself.graph.x.range()[1])return;var element=document.createElement("div");element.style.left=self.graph.x(o.value)+"px";element.classList.add("x_tick");element.classList.add(self.ticksTreatment);var title=document.createElement("div");title.classList.add("title");title.innerHTML=o.unit.formatter(new Date(o.value*1e3));element.appendChild(title);self.graph.element.appendChild(element);self.elements.push(element)})};this.graph.onUpdate(function(){self.render()})};Rickshaw.namespace("Rickshaw.Graph.Axis.X");Rickshaw.Graph.Axis.X=function(args){var self=this;var berthRate=.1;this.initialize=function(args){this.graph=args.graph;this.orientation=args.orientation||"top";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";if(args.element){this.element=args.element;this._discoverSize(args.element,args);this.vis=d3.select(args.element).append("svg:svg").attr("height",this.height).attr("width",this.width).attr("class","rickshaw_graph x_axis_d3");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}this.graph.onUpdate(function(){self.render()})};this.setSize=function(args){args=args||{};if(!this.element)return;this._discoverSize(this.element.parentNode,args);this.vis.attr("height",this.height).attr("width",this.width*(1+berthRate));var berth=Math.floor(this.width*berthRate/2);this.element.style.left=-1*berth+"px"};this.render=function(){if(this._renderWidth!==undefined&&this.graph.width!==this._renderWidth)this.setSize({auto:true});var axis=d3.svg.axis().scale(this.graph.x).orient(this.orientation);axis.tickFormat(args.tickFormat||function(x){return x});if(this.tickValues)axis.tickValues(this.tickValues);this.ticks=this.staticTicks||Math.floor(this.graph.width/this.pixelsPerTick);var berth=Math.floor(this.width*berthRate/2)||0;var transform;if(this.orientation=="top"){var yOffset=this.height||this.graph.height;transform="translate("+berth+","+yOffset+")"}else{transform="translate("+berth+", 0)"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["x_ticks_d3",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var gridSize=(this.orientation=="bottom"?1:-1)*this.graph.height;this.graph.vis.append("svg:g").attr("class","x_grid_d3").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-x-value",this.textContent)});this._renderHeight=this.graph.height};this._discoverSize=function(element,args){if(typeof window!=="undefined"){var style=window.getComputedStyle(element,null);var elementHeight=parseInt(style.getPropertyValue("height"),10);if(!args.auto){var elementWidth=parseInt(style.getPropertyValue("width"),10)}}this.width=(args.width||elementWidth||this.graph.width)*(1+berthRate);this.height=args.height||elementHeight||40};this.initialize(args)};Rickshaw.namespace("Rickshaw.Graph.Axis.Y");Rickshaw.Graph.Axis.Y=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.orientation=args.orientation||"right";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";this.tickFormat=args.tickFormat||function(y){return y};this.berthRate=.1;if(args.element){this.element=args.element;this.vis=d3.select(args.element).append("svg:svg").attr("class","rickshaw_graph y_axis");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}var self=this;this.graph.onUpdate(function(){self.render()})},setSize:function(args){args=args||{};if(!this.element)return;if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element.parentNode,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);if(!args.auto){var elementHeight=parseInt(style.getPropertyValue("height"),10)}}this.width=args.width||elementWidth||this.graph.width*this.berthRate;this.height=args.height||elementHeight||this.graph.height;this.vis.attr("width",this.width).attr("height",this.height*(1+this.berthRate));var berth=this.height*this.berthRate;if(this.orientation=="left"){this.element.style.top=-1*berth+"px"}},render:function(){if(this._renderHeight!==undefined&&this.graph.height!==this._renderHeight)this.setSize({auto:true});this.ticks=this.staticTicks||Math.floor(this.graph.height/this.pixelsPerTick);var axis=this._drawAxis(this.graph.y);this._drawGrid(axis);this._renderHeight=this.graph.height},_drawAxis:function(scale){var axis=d3.svg.axis().scale(scale).orient(this.orientation);axis.tickFormat(this.tickFormat);if(this.tickValues)axis.tickValues(this.tickValues);if(this.orientation=="left"){var berth=this.height*this.berthRate;var transform="translate("+this.width+", "+berth+")"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));return axis},_drawGrid:function(axis){var gridSize=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-y-value",this.textContent)
2 | })}});Rickshaw.namespace("Rickshaw.Graph.Axis.Y.Scaled");Rickshaw.Graph.Axis.Y.Scaled=Rickshaw.Class.create(Rickshaw.Graph.Axis.Y,{initialize:function($super,args){if(typeof args.scale==="undefined"){throw new Error("Scaled requires scale")}this.scale=args.scale;if(typeof args.grid==="undefined"){this.grid=true}else{this.grid=args.grid}$super(args)},_drawAxis:function($super,scale){var domain=this.scale.domain();var renderDomain=this.graph.renderer.domain().y;var extents=[Math.min.apply(Math,domain),Math.max.apply(Math,domain)];var extentMap=d3.scale.linear().domain([0,1]).range(extents);var adjExtents=[extentMap(renderDomain[0]),extentMap(renderDomain[1])];var adjustment=d3.scale.linear().domain(extents).range(adjExtents);var adjustedScale=this.scale.copy().domain(domain.map(adjustment)).range(scale.range());return $super(adjustedScale)},_drawGrid:function($super,axis){if(this.grid){$super(axis)}}});Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight");Rickshaw.Graph.Behavior.Series.Highlight=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;var colorSafe={};var activeLine=null;var disabledColor=args.disabledColor||function(seriesColor){return d3.interpolateRgb(seriesColor,d3.rgb("#d8d8d8"))(.8).toString()};this.addHighlightEvents=function(l){l.element.addEventListener("mouseover",function(e){if(activeLine)return;else activeLine=l;self.legend.lines.forEach(function(line){if(l===line){if(self.graph.renderer.unstack&&(line.series.renderer?line.series.renderer.unstack:true)){var seriesIndex=self.graph.series.indexOf(line.series);line.originalIndex=seriesIndex;var series=self.graph.series.splice(seriesIndex,1)[0];self.graph.series.push(series)}return}colorSafe[line.series.name]=colorSafe[line.series.name]||line.series.color;line.series.color=disabledColor(line.series.color)});self.graph.update()},false);l.element.addEventListener("mouseout",function(e){if(!activeLine)return;else activeLine=null;self.legend.lines.forEach(function(line){if(l===line&&line.hasOwnProperty("originalIndex")){var series=self.graph.series.pop();self.graph.series.splice(line.originalIndex,0,series);delete line.originalIndex}if(colorSafe[line.series.name]){line.series.color=colorSafe[line.series.name]}});self.graph.update()},false)};if(this.legend){this.legend.lines.forEach(function(l){self.addHighlightEvents(l)})}};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order");Rickshaw.Graph.Behavior.Series.Order=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;if(typeof window.jQuery=="undefined"){throw"couldn't find jQuery at window.jQuery"}if(typeof window.jQuery.ui=="undefined"){throw"couldn't find jQuery UI at window.jQuery.ui"}jQuery(function(){jQuery(self.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(event,ui){var series=[];jQuery(self.legend.list).find("li").each(function(index,item){if(!item.series)return;series.push(item.series)});for(var i=self.graph.series.length-1;i>=0;i--){self.graph.series[i]=series.shift()}self.graph.update()}});jQuery(self.legend.list).disableSelection()});this.graph.onUpdate(function(){var h=window.getComputedStyle(self.legend.element).height;self.legend.element.style.height=h})};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle");Rickshaw.Graph.Behavior.Series.Toggle=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;this.addAnchor=function(line){var anchor=document.createElement("a");anchor.innerHTML="✔";anchor.classList.add("action");line.element.insertBefore(anchor,line.element.firstChild);anchor.onclick=function(e){if(line.series.disabled){line.series.enable();line.element.classList.remove("disabled")}else{if(this.graph.series.filter(function(s){return!s.disabled}).length<=1)return;line.series.disable();line.element.classList.add("disabled")}}.bind(this);var label=line.element.getElementsByTagName("span")[0];label.onclick=function(e){var disableAllOtherLines=line.series.disabled;if(!disableAllOtherLines){for(var i=0;idomainX){dataIndex=Math.abs(domainX-data[i].x)0){alignables.forEach(function(el){el.classList.remove("left");el.classList.add("right")});var rightAlignError=this._calcLayoutError(alignables);if(rightAlignError>leftAlignError){alignables.forEach(function(el){el.classList.remove("right");el.classList.add("left")})}}if(typeof this.onRender=="function"){this.onRender(args)}},_calcLayoutError:function(alignables){var parentRect=this.element.parentNode.getBoundingClientRect();var error=0;var alignRight=alignables.forEach(function(el){var rect=el.getBoundingClientRect();if(!rect.width){return}if(rect.right>parentRect.right){error+=rect.right-parentRect.right}if(rect.left=self.previewWidth){frameAfterDrag[0]-=frameAfterDrag[1]-self.previewWidth;frameAfterDrag[1]=self.previewWidth}}self.graphs.forEach(function(graph){var domainScale=d3.scale.linear().interpolate(d3.interpolateNumber).domain([0,self.previewWidth]).range(graph.dataDomain());var windowAfterDrag=[domainScale(frameAfterDrag[0]),domainScale(frameAfterDrag[1])];self.slideCallbacks.forEach(function(callback){callback(graph,windowAfterDrag[0],windowAfterDrag[1])});if(frameAfterDrag[0]===0){windowAfterDrag[0]=undefined}if(frameAfterDrag[1]===self.previewWidth){windowAfterDrag[1]=undefined}graph.window.xMin=windowAfterDrag[0];graph.window.xMax=windowAfterDrag[1];graph.update()})}function onMousedown(){drag.target=d3.event.target;drag.start=self._getClientXFromEvent(d3.event,drag);self.frameBeforeDrag=self.currentFrame.slice();d3.event.preventDefault?d3.event.preventDefault():d3.event.returnValue=false;d3.select(document).on("mousemove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("mouseup.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchmove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("touchend.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",onMouseup)}function onMousedownLeftHandle(datum,index){drag.left=true;onMousedown()}function onMousedownRightHandle(datum,index){drag.right=true;onMousedown()}function onMousedownMiddleHandle(datum,index){drag.left=true;drag.right=true;drag.rigid=true;onMousedown()}function onMouseup(datum,index){d3.select(document).on("mousemove.rickshaw_range_slider_preview",null);d3.select(document).on("mouseup.rickshaw_range_slider_preview",null);d3.select(document).on("touchmove.rickshaw_range_slider_preview",null);d3.select(document).on("touchend.rickshaw_range_slider_preview",null);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",null);delete self.frameBeforeDrag;drag.left=false;drag.right=false;drag.rigid=false}element.select("rect.left_handle").on("mousedown",onMousedownLeftHandle);element.select("rect.right_handle").on("mousedown",onMousedownRightHandle);element.select("rect.middle_handle").on("mousedown",onMousedownMiddleHandle);element.select("rect.left_handle").on("touchstart",onMousedownLeftHandle);element.select("rect.right_handle").on("touchstart",onMousedownRightHandle);element.select("rect.middle_handle").on("touchstart",onMousedownMiddleHandle)},_getClientXFromEvent:function(event,drag){switch(event.type){case"touchstart":case"touchmove":var touchList=event.changedTouches;var touch=null;for(var touchIndex=0;touchIndexyMax)yMax=y});if(!series.length)return;if(series[0].xxMax)xMax=series[series.length-1].x});xMin-=(xMax-xMin)*this.padding.left;xMax+=(xMax-xMin)*this.padding.right;yMin=this.graph.min==="auto"?yMin:this.graph.min||0;yMax=this.graph.max===undefined?yMax:this.graph.max;if(this.graph.min==="auto"||yMin<0){yMin-=(yMax-yMin)*this.padding.bottom}if(this.graph.max===undefined){yMax+=(yMax-yMin)*this.padding.top}return{x:[xMin,xMax],y:[yMin,yMax]}},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var pathNodes=vis.selectAll("path.path").data(data).enter().append("svg:path").classed("path",true).attr("d",this.seriesPathFactory());if(this.stroke){var strokeNodes=vis.selectAll("path.stroke").data(data).enter().append("svg:path").classed("stroke",true).attr("d",this.seriesStrokeFactory())}var i=0;series.forEach(function(series){if(series.disabled)return;series.path=pathNodes[0][i];if(this.stroke)series.stroke=strokeNodes[0][i];this._styleSeries(series);i++},this)},_styleSeries:function(series){var fill=this.fill?series.color:"none";var stroke=this.stroke?series.color:"none";series.path.setAttribute("fill",fill);series.path.setAttribute("stroke",stroke);series.path.setAttribute("stroke-width",this.strokeWidth);if(series.className){d3.select(series.path).classed(series.className,true)}if(series.className&&this.stroke){d3.select(series.stroke).classed(series.className,true)}},configure:function(args){args=args||{};Rickshaw.keys(this.defaults()).forEach(function(key){if(!args.hasOwnProperty(key)){this[key]=this[key]||this.graph[key]||this.defaults()[key];return}if(typeof this.defaults()[key]=="object"){Rickshaw.keys(this.defaults()[key]).forEach(function(k){this[key][k]=args[key][k]!==undefined?args[key][k]:this[key][k]!==undefined?this[key][k]:this.defaults()[key][k]},this)}else{this[key]=args[key]!==undefined?args[key]:this[key]!==undefined?this[key]:this.graph[key]!==undefined?this.graph[key]:this.defaults()[key]}},this)},setStrokeWidth:function(strokeWidth){if(strokeWidth!==undefined){this.strokeWidth=strokeWidth}},setTension:function(tension){if(tension!==undefined){this.tension=tension}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Line");Rickshaw.Graph.Renderer.Line=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"line",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Stack");Rickshaw.Graph.Renderer.Stack=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"stack",defaults:function($super){return Rickshaw.extend($super(),{fill:true,stroke:false,unstack:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Bar");Rickshaw.Graph.Renderer.Bar=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"bar",defaults:function($super){var defaults=Rickshaw.extend($super(),{gapSize:.05,unstack:false});delete defaults.tension;return defaults},initialize:function($super,args){args=args||{};this.gapSize=args.gapSize||this.gapSize;$super(args)},domain:function($super){var domain=$super();var frequentInterval=this._frequentInterval(this.graph.stackedData.slice(-1).shift());domain.x[1]+=Number(frequentInterval.magnitude);return domain},barWidth:function(series){var frequentInterval=this._frequentInterval(series.stack);var barWidth=this.graph.x.magnitude(frequentInterval.magnitude)*(1-this.gapSize);return barWidth},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var barWidth=this.barWidth(series.active()[0]);var barXOffset=0;var activeSeriesCount=series.filter(function(s){return!s.disabled}).length;var seriesBarWidth=this.unstack?barWidth/activeSeriesCount:barWidth;var transform=function(d){var matrix=[1,0,0,d.y<0?-1:1,0,d.y<0?graph.y.magnitude(Math.abs(d.y))*2:0];return"matrix("+matrix.join(",")+")"};series.forEach(function(series){if(series.disabled)return;var barWidth=this.barWidth(series);var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:rect").attr("x",function(d){return graph.x(d.x)+barXOffset}).attr("y",function(d){return graph.y(d.y0+Math.abs(d.y))*(d.y<0?-1:1)}).attr("width",seriesBarWidth).attr("height",function(d){return graph.y.magnitude(Math.abs(d.y))}).attr("transform",transform);Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)});if(this.unstack)barXOffset+=seriesBarWidth},this)},_frequentInterval:function(data){var intervalCounts={};for(var i=0;i0){this[0].data.forEach(function(plot){item.data.push({x:plot.x,y:0})})}else if(item.data.length===0){item.data.push({x:this.timeBase-(this.timeInterval||0),y:0})}this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},addData:function(data,x){var index=this.getIndex();Rickshaw.keys(data).forEach(function(name){if(!this.itemByName(name)){this.addItem({name:name})}},this);this.forEach(function(item){item.data.push({x:x||(index*this.timeInterval||1)+this.timeBase,y:data[item.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(name){for(var i=0;i1;i--){this.currentSize+=1;this.currentIndex+=1;this.forEach(function(item){item.data.unshift({x:((i-1)*this.timeInterval||1)+this.timeBase,y:0,i:i})},this)}}},addData:function($super,data,x){$super(data,x);this.currentSize+=1;this.currentIndex+=1;if(this.maxDataPoints!==undefined){while(this.currentSize>this.maxDataPoints){this.dropData()}}},dropData:function(){this.forEach(function(item){item.data.splice(0,1)});this.currentSize-=1},getIndex:function(){return this.currentIndex}});return Rickshaw});
4 |
--------------------------------------------------------------------------------
/frontend/lib/public/lib/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding-left: 20px
3 | }
4 |
5 | h2 {
6 | color: #555555;
7 | font-size: 28px;
8 | font-weight: normal;
9 | float: none;
10 | text-shadow: 0 1px 0 #ffffff;
11 | position: relative;
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/lib/webStream.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const WebSocket = require('websocket-stream');
4 | const Eos = require('end-of-stream');
5 |
6 |
7 | module.exports = function (server) {
8 | let streamCounter = 0;
9 | const streams = {};
10 |
11 | const emit = (data) => {
12 | const ids = Object.keys(streams);
13 | ids.forEach((id) => {
14 | streams[id].write(JSON.stringify(data), () => {});
15 | });
16 | };
17 |
18 | const handleStream = (stream) => {
19 | stream.id = streamCounter++;
20 | streams[stream.id] = stream;
21 |
22 | Eos(stream, () => {
23 | delete streams[stream.id];
24 | });
25 | };
26 |
27 |
28 | WebSocket.createServer({ server }, handleStream);
29 |
30 | return { emit };
31 | };
32 |
--------------------------------------------------------------------------------
/frontend/memory.sh:
--------------------------------------------------------------------------------
1 | #!/bin/ash
2 | # check free memory
3 | total=$(free | awk -F' +' '/Mem/{print $3}')
4 | /bin/containerpilot -putmetric "example_frontend_free_memory=$total"
5 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "./lib/index.js",
6 | "keywords": [],
7 | "author": "",
8 | "license": "MPL-2.0",
9 | "dependencies": {
10 | "brule": "2.x.x",
11 | "end-of-stream": "1.x.x",
12 | "hapi": "16.x.x",
13 | "inert": "4.x.x",
14 | "piloted": "3.x.x",
15 | "websocket-stream": "3.x.x",
16 | "wreck": "12.x.x"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/local-compose.yml:
--------------------------------------------------------------------------------
1 | consul:
2 | image: autopilotpattern/consul:0.7.3r39
3 | restart: always
4 | dns:
5 | - 127.0.0.1
6 | labels:
7 | - triton.cns.services=consul
8 | ports:
9 | - "8500:8500"
10 | command: >
11 | /usr/local/bin/containerpilot
12 | environment:
13 | - CONSUL_DEV=1
14 | prometheus:
15 | image: autopilotpattern/prometheus:1.7.1-r20
16 | restart: always
17 | ports:
18 | - "9090:9090"
19 | links:
20 | - consul:consul
21 | environment:
22 | - CONSUL=consul
23 | - CONSUL_AGENT=1
24 | dns:
25 | - 127.0.0.1
26 | nats:
27 | image: autopilotpattern/nats:0.9.6-r1.0.0
28 | restart: always
29 | ports:
30 | - 8222
31 | links:
32 | - consul:consul
33 | environment:
34 | - CONSUL=consul
35 | - CONSUL_AGENT=1
36 | - NATS_USER=ruser
37 | - NATS_PASSWORD=password
38 | natsboard:
39 | image: d0cker/natsboard
40 | restart: always
41 | ports:
42 | - "3000:3000"
43 | - "3001:3001"
44 | links:
45 | - consul:consul
46 | environment:
47 | - CONSUL=consul
48 | - CONSUL_AGENT=1
49 | traefik:
50 | build: ./traefik
51 | ports:
52 | - "80:80"
53 | - "8080:8080"
54 | links:
55 | - consul:consul
56 | environment:
57 | - CONSUL=consul
58 | restart: always
59 | influxdb:
60 | image: autopilotpattern/influxdb:1.1.1
61 | ports:
62 | - "8086:8086"
63 | - "8083:8083"
64 | restart: always
65 | links:
66 | - consul:consul
67 | environment:
68 | - CONSUL=consul
69 | - ADMIN_USER=root
70 | - INFLUXDB_INIT_PWD=root123
71 | - INFLUXDB_ADMIN_ENABLED=true
72 | - INFLUXDB_REPORTING_DISABLED=true
73 | - INFLUXDB_DATA_QUERY_LOG_ENABLED=false
74 | - INFLUXDB_HTTP_LOG_ENABLED=false
75 | - INFLUXDB_CONTINUOUS_QUERIES_LOG_ENABLED=false
76 | serializer:
77 | build: ./serializer
78 | links:
79 | - consul:consul
80 | environment:
81 | - PORT=10000
82 | - CONSUL=consul
83 | - INFLUXDB_USER=root
84 | - INFLUXDB_PWD=root123
85 | expose:
86 | - 10000
87 | - 9090
88 | restart: always
89 | frontend:
90 | build: ./frontend
91 | links:
92 | - consul:consul
93 | environment:
94 | - CONSUL=consul
95 | - PORT=10001
96 | expose:
97 | - 10001
98 | - 9090
99 | restart: always
100 | smartthings:
101 | build: ./smartthings
102 | links:
103 | - consul:consul
104 | expose:
105 | - 8080
106 | environment:
107 | - PORT=8080
108 | - CONSUL=consul
109 | - NATS_USER=ruser
110 | - NATS_PASSWORD=password
111 | - FAKE_MODE=true
112 | restart: always
113 | humidity:
114 | build: ./sensor
115 | links:
116 | - consul:consul
117 | environment:
118 | - SENSOR_TYPE=humidity
119 | - CONSUL=consul
120 | - NATS_USER=ruser
121 | - NATS_PASSWORD=password
122 | restart: always
123 | motion:
124 | build: ./sensor
125 | links:
126 | - consul:consul
127 | environment:
128 | - SENSOR_TYPE=motion
129 | - CONSUL=consul
130 | - NATS_USER=ruser
131 | - NATS_PASSWORD=password
132 | restart: always
133 | temperature:
134 | build: ./sensor
135 | links:
136 | - consul:consul
137 | environment:
138 | - SENSOR_TYPE=temperature
139 | - CONSUL=consul
140 | - NATS_USER=ruser
141 | - NATS_PASSWORD=password
142 | restart: always
143 |
--------------------------------------------------------------------------------
/project_overview.graffle/data.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/data.plist
--------------------------------------------------------------------------------
/project_overview.graffle/image1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image1.pdf
--------------------------------------------------------------------------------
/project_overview.graffle/image15.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image15.tiff
--------------------------------------------------------------------------------
/project_overview.graffle/image16.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image16.tiff
--------------------------------------------------------------------------------
/project_overview.graffle/image17.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image17.tiff
--------------------------------------------------------------------------------
/project_overview.graffle/image18.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image18.tiff
--------------------------------------------------------------------------------
/project_overview.graffle/image19.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image19.tiff
--------------------------------------------------------------------------------
/project_overview.graffle/image2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image2.png
--------------------------------------------------------------------------------
/project_overview.graffle/image22.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image22.tiff
--------------------------------------------------------------------------------
/project_overview.graffle/image3.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image3.tiff
--------------------------------------------------------------------------------
/project_overview.graffle/image6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image6.png
--------------------------------------------------------------------------------
/project_overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.png
--------------------------------------------------------------------------------
/sensor/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8-alpine
2 |
3 | RUN apk update && \
4 | apk add curl && \
5 | rm -rf /var/cache/apk/*
6 |
7 | RUN export CONSUL_VERSION=0.7.0 \
8 | && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \
9 | && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \
10 | && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \
11 | && unzip /tmp/consul -d /usr/local/bin \
12 | && rm /tmp/consul.zip \
13 | && mkdir /config
14 |
15 | # Install ContainerPilot
16 | ENV CONTAINERPILOT_VERSION 3.4.3
17 | RUN export CP_SHA1=e8258ed166bcb3de3e06638936dcc2cae32c7c58 \
18 | && curl -Lso /tmp/containerpilot.tar.gz \
19 | "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \
20 | && echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \
21 | && tar zxf /tmp/containerpilot.tar.gz -C /bin \
22 | && rm /tmp/containerpilot.tar.gz
23 |
24 | # COPY ContainerPilot configuration
25 | ENV CONTAINERPILOT_PATH=/etc/containerpilot.json5
26 | COPY containerpilot.json5 ${CONTAINERPILOT_PATH}
27 | ENV CONTAINERPILOT=${CONTAINERPILOT_PATH}
28 |
29 | # Install our application
30 | RUN mkdir -p /opt/app/lib
31 | COPY package.json /opt/app/
32 | COPY lib/* /opt/app/lib/
33 | RUN cd /opt/app && npm install
34 |
35 | CMD ["/bin/containerpilot"]
36 |
--------------------------------------------------------------------------------
/sensor/containerpilot.json5:
--------------------------------------------------------------------------------
1 | {
2 | consul: 'localhost:8500',
3 | jobs: [
4 | {
5 | name: '{{.SENSOR_TYPE}}',
6 | exec: 'node /opt/app/',
7 | restarts: 'unlimited',
8 | health: {
9 | exec: 'pgrep node',
10 | interval: 10,
11 | ttl: 20
12 | }
13 | },
14 | {
15 | name: 'consul-agent',
16 | exec: ['/usr/local/bin/consul', 'agent',
17 | '-data-dir=/data',
18 | '-config-dir=/config',
19 | '-log-level=err',
20 | '-rejoin',
21 | '-retry-join', '{{ .CONSUL | default "consul" }}',
22 | '-retry-max', '10',
23 | '-retry-interval', '10s'],
24 | restarts: 'unlimited'
25 | },
26 | {
27 | name: 'onchange-serializer',
28 | exec: 'pkill -SIGHUP node',
29 | when: {
30 | source: 'watch.serializer',
31 | each: 'changed'
32 | }
33 | },
34 | {
35 | name: 'onchange-nats',
36 | exec: 'pkill -SIGHUP node',
37 | when: {
38 | source: 'watch.nats',
39 | each: 'changed'
40 | }
41 | }
42 | ],
43 | telemetry: {
44 | port: 9090,
45 | interfaces: ['eth1', 'eth0', 'eth0[1]', 'lo', 'lo0', 'inet']
46 | },
47 | watches: [
48 | {
49 | name: 'serializer',
50 | interval: 1
51 | },
52 | {
53 | name: 'nats',
54 | interval: 3
55 | }
56 | ]
57 | }
58 |
--------------------------------------------------------------------------------
/sensor/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Load modules
4 |
5 | const Nats = require('nats');
6 | const Piloted = require('piloted');
7 | const Wreck = require('wreck');
8 |
9 |
10 | const internals = {
11 | type: process.env.SENSOR_TYPE
12 | };
13 |
14 | function setupNats() {
15 | const servers = Piloted.serviceHosts('nats');
16 |
17 | if (!servers || !servers.length) {
18 | console.error('NATS not found');
19 | return setTimeout(() => { setupNats(); }, 1000);
20 | }
21 |
22 | const natsServers = servers.map((server) => {
23 | return `nats://${process.env.NATS_USER}:${process.env.NATS_PASSWORD}@${server.address}:4222`;
24 | });
25 |
26 | const nats = Nats.connect({ servers: natsServers });
27 | nats.on('error', (err) => {
28 | console.log(err);
29 | });
30 |
31 | // Subscribe for messages related to the sensor type subject
32 | // and create a queue group so that multiple instances don't
33 | // handle the same message
34 | nats.subscribe(internals.type, { queue: 'sensor' }, writeData);
35 | }
36 |
37 | function writeData (data) {
38 | const serializer = Piloted.service('serializer');
39 |
40 | if (!serializer) {
41 | console.error('Serializer not found');
42 | return setTimeout(() => { writeData(data); }, 1000);
43 | }
44 |
45 | Wreck.post(`http://${serializer.address}:${serializer.port}/write/${internals.type}`, { payload: data }, (err) => {
46 | if (err) {
47 | console.error(err);
48 | }
49 | });
50 | }
51 |
52 | Piloted.on('refresh', () => {
53 | setupNats();
54 | });
55 |
56 | setupNats();
57 |
--------------------------------------------------------------------------------
/sensor/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "temperature",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "lib/index.js",
6 | "author": "",
7 | "license": "MPL-2.0",
8 | "dependencies": {
9 | "nats": "0.7.x",
10 | "piloted": "3.x.x",
11 | "wreck": "12.x.x"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/serializer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8-alpine
2 |
3 | RUN apk update && \
4 | apk add curl && \
5 | rm -rf /var/cache/apk/*
6 |
7 | RUN export CONSUL_VERSION=0.7.0 \
8 | && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \
9 | && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \
10 | && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \
11 | && unzip /tmp/consul -d /usr/local/bin \
12 | && rm /tmp/consul.zip \
13 | && mkdir /config
14 |
15 | # Install ContainerPilot
16 | ENV CONTAINERPILOT_VERSION 3.4.2
17 | RUN export CP_SHA1=5c99ae9ede01e8fcb9b027b5b3cb0cfd8c0b8b88 \
18 | && curl -Lso /tmp/containerpilot.tar.gz \
19 | "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \
20 | && echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \
21 | && tar zxf /tmp/containerpilot.tar.gz -C /bin \
22 | && rm /tmp/containerpilot.tar.gz
23 |
24 | # COPY ContainerPilot configuration
25 | ENV CONTAINERPILOT_PATH=/etc/containerpilot.json5
26 | COPY containerpilot.json5 ${CONTAINERPILOT_PATH}
27 | ENV CONTAINERPILOT=${CONTAINERPILOT_PATH}
28 |
29 | RUN mkdir -p /bin/sensors
30 | COPY sensors/* /bin/sensors/
31 | RUN chmod 755 /bin/sensors/*.sh
32 | RUN mkdir -p /var/log/app
33 |
34 | # Install our application
35 | RUN mkdir -p /opt/app/lib
36 | COPY package.json /opt/app/
37 | COPY lib/* /opt/app/lib/
38 | RUN cd /opt/app && npm install
39 |
40 | CMD ["/bin/containerpilot"]
41 |
--------------------------------------------------------------------------------
/serializer/containerpilot.json5:
--------------------------------------------------------------------------------
1 | {
2 | consul: 'localhost:8500',
3 | jobs: [
4 | {
5 | name: 'serializer',
6 | port: {{.PORT}},
7 | exec: 'node /opt/app/',
8 | health: {
9 | exec: '/usr/bin/curl -o /dev/null --fail -s http://127.0.0.1:{{.PORT}}/check-it-out',
10 | interval: 1,
11 | ttl: 2
12 | },
13 | restarts: 'unlimited'
14 | },
15 | {
16 | name: 'consul-agent',
17 | exec: ['/usr/local/bin/consul', 'agent',
18 | '-data-dir=/data',
19 | '-config-dir=/config',
20 | '-log-level=err',
21 | '-rejoin',
22 | '-retry-join', '{{ .CONSUL | default "consul" }}',
23 | '-retry-max', '10',
24 | '-retry-interval', '10s'],
25 | restarts: 'unlimited'
26 | },
27 | {
28 | name: 'onchange-influxdb',
29 | exec: 'pkill -SIGHUP node',
30 | when: {
31 | source: 'watch.influxdb',
32 | each: 'changed'
33 | }
34 | },
35 | {
36 | name: 'requests-sensor',
37 | exec: '/bin/sensors/request_count.sh',
38 | timeout: '5s',
39 | when: {
40 | interval: '5s'
41 | },
42 | restarts: 'unlimited'
43 | }
44 | ],
45 | watches: [
46 | {
47 | name: 'influxdb',
48 | interval: 2
49 | }
50 | ],
51 | telemetry: {
52 | port: 9090,
53 | tags: ['op'],
54 | interfaces: ['eth1', 'eth0', 'eth0[1]', 'lo', 'lo0', 'inet'],
55 | metrics: [
56 | {
57 | namespace: 'example',
58 | subsystem: 'serializer',
59 | name: 'requests',
60 | help: 'Number of requests/5s',
61 | type: 'gauge'
62 | },
63 | {
64 | namespace: 'example',
65 | subsystem: 'process',
66 | name: 'event_delay',
67 | help: 'Node.js event loop delay',
68 | type: 'gauge'
69 | }
70 | ]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/serializer/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Brule = require('brule');
4 | const FlattenDeep = require('lodash.flattendeep');
5 | const Good = require('good');
6 | const Hapi = require('hapi');
7 | const Influx = require('influx');
8 | const Items = require('items');
9 | const Piloted = require('piloted');
10 | const Toppsy = require('toppsy');
11 |
12 |
13 | const internals = {
14 | dbName: 'sensors',
15 | failCount: 0
16 | };
17 |
18 | const bufferedDb = {
19 | draining: false,
20 | data: [],
21 | writePoints: function (points) {
22 | console.log('Write in dummy db');
23 | bufferedDb.data = bufferedDb.data.concat(points);
24 | return new Promise((resolve) => {
25 | resolve();
26 | });
27 | },
28 | query: function(query) {
29 | return new Promise((resolve) => {
30 | resolve(bufferedDb.data);
31 | });
32 | },
33 | drain: function () {
34 | if (bufferedDb.draining) {
35 | return;
36 | }
37 |
38 | bufferedDb.draining = true;
39 | Items.serial(bufferedDb.data, (data, next) => {
40 | writePoint(data.measurement, data.fields.value, next);
41 | }, (err) => {
42 | bufferedDb.data = [];
43 | bufferedDb.draining = false;
44 | });
45 | }
46 | };
47 |
48 |
49 | function main () {
50 | setupDb();
51 | setupHapi();
52 | }
53 | main();
54 |
55 |
56 | function setupHapi () {
57 | const server = new Hapi.Server({
58 | load: {
59 | sampleInterval: 100
60 | }
61 | });
62 |
63 | server.connection({
64 | port: process.env.PORT,
65 | load: {
66 | maxEventLoopDelay: 100 // 50 milliseconds
67 | }
68 | });
69 |
70 | const goodOptions = {
71 | reporters: {
72 | fileReporter: [{
73 | module: 'good-squeeze',
74 | name: 'Squeeze',
75 | args: [{ response: '*' }]
76 | }, {
77 | module: 'good-squeeze',
78 | name: 'SafeJson'
79 | }, {
80 | module: 'good-file',
81 | args: ['/var/log/app/requests.log']
82 | }]
83 | }
84 | };
85 |
86 |
87 | server.register([
88 | Brule,
89 | { register: Good, options: goodOptions },
90 | { register: Toppsy, options: { namespace: 'example' } }
91 | ], (err) => {
92 | if (err) {
93 | console.error(err);
94 | process.exit(1);
95 | }
96 |
97 | server.route({
98 | method: 'POST',
99 | path: '/write/{type}',
100 | handler: writeHandler
101 | });
102 |
103 | server.route({
104 | method: 'GET',
105 | path: '/read',
106 | handler: readHandler
107 | });
108 |
109 | server.start((err) => {
110 | if (err) {
111 | console.error(err);
112 | process.exit(1);
113 | }
114 |
115 | console.log(`Hapi server started at http://localhost:${server.info.port}`);
116 | });
117 | });
118 | }
119 |
120 | function writeHandler (request, reply) {
121 | writePoint(request.params.type, request.payload.value, (err) => {
122 | reply({ err });
123 | });
124 | }
125 |
126 | function readHandler (request, reply) {
127 | let results = [];
128 | Items.parallel(['motion', 'humidity', 'temperature'], (type, next) => {
129 | readPoints(type, 1, (err, result) => {
130 | results = results.concat(result);
131 | next();
132 | });
133 | }, () => {
134 | reply(FlattenDeep(results));
135 | });
136 | }
137 |
138 | function setupDb () {
139 | const influxServer = Piloted.service('influxdb');
140 | if (!influxServer && internals.failCount > 10) {
141 | internals.failCount = 0;
142 | Piloted.refresh();
143 | }
144 |
145 | if (!influxServer) {
146 | internals.failCount++;
147 | internals.db = bufferedDb;
148 | return setTimeout(setupDb, 1000);
149 | }
150 |
151 | internals.db = new Influx.InfluxDB({
152 | host: influxServer.address,
153 | port: influxServer.port,
154 | username: process.env.INFLUXDB_USER,
155 | password: process.env.INFLUXDB_PWD
156 | });
157 |
158 | internals.db.createDatabase(internals.dbName)
159 | .then(() => {
160 | bufferedDb.drain();
161 | })
162 | .catch((err) => {
163 | console.error(`Error creating Influx database!`);
164 | console.error(err);
165 |
166 | // Influx may not be entirely ready
167 | setTimeout(setupDb, 1000);
168 | });
169 | }
170 |
171 | Piloted.on('refresh', () => {
172 | setupDb();
173 | });
174 |
175 |
176 | function writePoint (type, value, cb) {
177 | internals.db.writePoints([{ measurement: type, fields: { value } }], { database: internals.dbName })
178 | .then(() => {
179 | return cb();
180 | })
181 | .catch((err) => {
182 | return cb(err);
183 | });
184 | };
185 |
186 | function readPoints (type, ago, cb) {
187 | ago = ago || 1;
188 | const query = `select * from ${type} where time > now() - ${ago}m`;
189 | internals.db.query(query, { database: internals.dbName })
190 | .then((rows) => {
191 | if (rows && rows.length) {
192 | for (let i = 0; i < rows.length; ++i) {
193 | rows[i].type = type;
194 | }
195 | }
196 | cb(null, rows);
197 | })
198 | .catch((err) => {
199 | console.error('Error querying db: ' + err);
200 | return cb(err);
201 | });
202 | };
203 |
--------------------------------------------------------------------------------
/serializer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "serializer",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "lib/index.js",
6 | "author": "",
7 | "license": "MPL-2.0",
8 | "dependencies": {
9 | "brule": "2.x.x",
10 | "good": "7.x.x",
11 | "good-file": "6.x.x",
12 | "good-squeeze": "5.x.x",
13 | "hapi": "16.x.x",
14 | "influx": "5.x.x",
15 | "items": "2.x.x",
16 | "lodash.flattendeep": "4.x.x",
17 | "piloted": "3.x.x",
18 | "toppsy": "^1.1.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/serializer/sensors/request_count.sh:
--------------------------------------------------------------------------------
1 | #!/bin/ash
2 |
3 | FILE=/var/log/app/requests.log
4 | count=$(wc -l $FILE | awk '{printf $1}')
5 | truncate -s 0 $FILE
6 | ./bin/containerpilot -putmetric "example_serializer_requests=$count"
7 |
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e -o pipefail
3 |
4 | help() {
5 | echo
6 | echo 'Usage ./setup.sh'
7 | echo
8 | echo 'Checks that your Triton and Docker environment is sane and configures'
9 | echo 'an environment file to use.'
10 | echo
11 | }
12 |
13 |
14 | # populated by `check` function whenever we're using Triton
15 | TRITON_USER=
16 | TRITON_DC=
17 | TRITON_ACCOUNT=
18 |
19 | # ---------------------------------------------------
20 | # Top-level commands
21 |
22 | # Check for correct configuration and setup _env file
23 | envcheck() {
24 |
25 | command -v docker >/dev/null 2>&1 || {
26 | echo
27 | tput rev # reverse
28 | tput bold # bold
29 | echo 'Docker is required, but does not appear to be installed.'
30 | tput sgr0 # clear
31 | echo 'See https://docs.joyent.com/public-cloud/api-access/docker'
32 | exit 1
33 | }
34 | command -v json >/dev/null 2>&1 || {
35 | echo
36 | tput rev # reverse
37 | tput bold # bold
38 | echo 'Error! JSON CLI tool is required, but does not appear to be installed.'
39 | tput sgr0 # clear
40 | echo 'See https://apidocs.joyent.com/cloudapi/#getting-started'
41 | exit 1
42 | }
43 |
44 | command -v triton >/dev/null 2>&1 || {
45 | echo
46 | tput rev # reverse
47 | tput bold # bold
48 | echo 'Error! Joyent Triton CLI is required, but does not appear to be installed.'
49 | tput sgr0 # clear
50 | echo 'See https://www.joyent.com/blog/introducing-the-triton-command-line-tool'
51 | exit 1
52 | }
53 |
54 | # make sure Docker client is pointed to the same place as the Triton client
55 | local docker_user=$(docker info 2>&1 | awk -F": " '/SDCAccount:/{print $2}')
56 | local docker_dc=$(echo $DOCKER_HOST | awk -F"/" '{print $3}' | awk -F'.' '{print $1}')
57 | TRITON_USER=$(triton profile get | awk -F": " '/account:/{print $2}')
58 | TRITON_DC=$(triton profile get | awk -F"/" '/url:/{print $3}' | awk -F'.' '{print $1}')
59 | TRITON_ACCOUNT=$(triton account get | awk -F": " '/id:/{print $2}')
60 |
61 | local triton_cns_enabled=$(triton account get | awk -F": " '/cns/{print $2}')
62 | if [ ! "true" == "$triton_cns_enabled" ]; then
63 | echo
64 | tput rev # reverse
65 | tput bold # bold
66 | echo 'Error! Triton CNS is required and not enabled.'
67 | tput sgr0 # clear
68 | echo
69 | exit 1
70 | fi
71 |
72 | # setup environment file
73 | if [ ! -f "_env" ]; then
74 | echo 'CONSUL_AGENT=1' > _env
75 | echo 'LOG_LEVEL=INFO' >> _env
76 | echo 'PORT=80' >> _env
77 | echo >> _env
78 |
79 | set +o pipefail
80 |
81 | echo '# Consul discovery via Triton CNS' >> _env
82 | echo CONSUL=consul.svc.${TRITON_ACCOUNT}.${TRITON_DC}.cns.joyent.com >> _env
83 | echo >> _env
84 |
85 | echo '# NATS auth settings' >> _env
86 | echo 'NATS_USER=ruser' >> _env
87 | echo 'NATS_PASSWORD='$(cat /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 7) >> _env
88 | echo >> _env
89 |
90 | echo 'Edit the _env file with your desired config'
91 | else
92 | echo 'Existing _env file found, exiting'
93 | exit
94 | fi
95 | }
96 |
97 | get_root_password() {
98 | echo $(docker logs ${COMPOSE_PROJECT_NAME:-influxdb}_influxdb_1 2>&1 | \
99 | awk '/Generated root password/{print $NF}' | \
100 | awk '{$1=$1};1'
101 | ) | pbcopy
102 | }
103 |
104 |
105 |
106 | # ---------------------------------------------------
107 | # parse arguments
108 |
109 | # Get function list
110 | funcs=($(declare -F -p | cut -d " " -f 3))
111 |
112 | until
113 | if [ ! -z "$1" ]; then
114 | # check if the first arg is a function in this file, or use a default
115 | if [[ " ${funcs[@]} " =~ " $1 " ]]; then
116 | cmd=$1
117 | shift 1
118 | else
119 | cmd="envcheck"
120 | fi
121 |
122 | $cmd "$@"
123 | if [ $? == 127 ]; then
124 | help
125 | fi
126 |
127 | exit
128 | else
129 | envcheck
130 | fi
131 | do
132 | echo
133 | done
134 |
--------------------------------------------------------------------------------
/smartthings/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8-alpine
2 |
3 | RUN apk update && \
4 | apk add curl && \
5 | rm -rf /var/cache/apk/*
6 |
7 | RUN export CONSUL_VERSION=0.7.0 \
8 | && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \
9 | && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \
10 | && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \
11 | && unzip /tmp/consul -d /usr/local/bin \
12 | && rm /tmp/consul.zip \
13 | && mkdir /config
14 |
15 | # Install ContainerPilot
16 | ENV CONTAINERPILOT_VERSION 3.4.3
17 | RUN export CP_SHA1=e8258ed166bcb3de3e06638936dcc2cae32c7c58 \
18 | && curl -Lso /tmp/containerpilot.tar.gz \
19 | "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \
20 | && echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \
21 | && tar zxf /tmp/containerpilot.tar.gz -C /bin \
22 | && rm /tmp/containerpilot.tar.gz
23 |
24 | # COPY ContainerPilot configuration
25 | ENV CONTAINERPILOT_PATH=/etc/containerpilot.json5
26 | COPY containerpilot.json5 ${CONTAINERPILOT_PATH}
27 | ENV CONTAINERPILOT=${CONTAINERPILOT_PATH}
28 |
29 | # Install our application
30 | RUN mkdir -p /opt/app/lib
31 | COPY package.json /opt/app/
32 | COPY health.sh /opt/app/
33 | RUN chmod 755 /opt/app/health.sh
34 | COPY lib/* /opt/app/lib/
35 | RUN cd /opt/app && npm install
36 |
37 | CMD ["/bin/containerpilot"]
38 |
--------------------------------------------------------------------------------
/smartthings/containerpilot.json5:
--------------------------------------------------------------------------------
1 | {
2 | consul: 'localhost:8500',
3 | jobs: [
4 | {
5 | name: 'smartthings',
6 | port: {{.PORT}},
7 | exec: 'node /opt/app/',
8 | health: {
9 | exec: '/opt/app/health.sh',
10 | interval: 5,
11 | ttl: 30
12 | },
13 | restarts: 'unlimited'
14 | },
15 | {
16 | name: 'consul-agent',
17 | exec: ['/usr/local/bin/consul', 'agent',
18 | '-data-dir=/data',
19 | '-config-dir=/config',
20 | '-log-level=err',
21 | '-rejoin',
22 | '-retry-join', '{{ .CONSUL | default "consul" }}',
23 | '-retry-max', '10',
24 | '-retry-interval', '10s'],
25 | restarts: 'unlimited'
26 | },
27 | {
28 | name: 'onchange-nats',
29 | exec: 'pkill -SIGHUP node',
30 | when: {
31 | source: 'watch.nats',
32 | each: 'changed'
33 | }
34 | }
35 | ],
36 | telemetry: {
37 | port: 9090,
38 | interfaces: ['eth1', 'eth0', 'eth0[1]', 'lo', 'lo0', 'inet']
39 | },
40 | watches: [
41 | {
42 | name: 'nats',
43 | interval: 3
44 | }
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/smartthings/health.sh:
--------------------------------------------------------------------------------
1 | #!/bin/ash
2 |
3 | /usr/bin/curl -o /dev/null --fail -s -H 'Content-Type: application/json' -d '{ "role": "seneca", "cmd": "stats" }' http://localhost:$PORT/act
4 |
--------------------------------------------------------------------------------
/smartthings/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Nats = require('nats');
4 | const Piloted = require('piloted');
5 | const Seneca = require('seneca');
6 |
7 |
8 | const internals = {};
9 |
10 | function setupSeneca() {
11 | const seneca = Seneca();
12 | const passcode = process.env.PASSCODE || 'secure';
13 |
14 | seneca.add({ role: 'smartthings', cmd: 'write', type: 'humidity', passcode }, (args, cb) => {
15 | if (!internals.nats) {
16 | return cb();
17 | }
18 |
19 | internals.nats.publish(type, JSON.stringify({
20 | type: 'humidity',
21 | time: Date.now(),
22 | value: args.value
23 | }));
24 | cb();
25 | });
26 |
27 | seneca.add({ role: 'smartthings', cmd: 'write', type: 'motion', passcode }, (args, cb) => {
28 | if (!internals.nats) {
29 | return cb();
30 | }
31 |
32 | internals.nats.publish(type, JSON.stringify({
33 | type: 'motion',
34 | time: Date.now(),
35 | value: args.value
36 | }));
37 | cb();
38 | });
39 |
40 | seneca.add({ role: 'smartthings', cmd: 'write', type: 'temperature', passcode }, (args, cb) => {
41 | if (!internals.nats) {
42 | return cb();
43 | }
44 | internals.nats.publish(type, JSON.stringify({
45 | type: 'temperature',
46 | time: Date.now(),
47 | value: args.value
48 | }));
49 | cb();
50 | });
51 |
52 | seneca.listen({ port: process.env.PORT });
53 | }
54 |
55 | function setupNats(cb) {
56 | cb = cb || function () {};
57 | const servers = Piloted.serviceHosts('nats');
58 |
59 | if (!servers || !servers.length) {
60 | console.error('NATS not found');
61 | return setTimeout(() => { setupNats(cb); }, 1000);
62 | }
63 |
64 | const natsServers = servers.map((server) => {
65 | return `nats://${process.env.NATS_USER}:${process.env.NATS_PASSWORD}@${server.address}:4222`;
66 | });
67 |
68 | internals.nats = Nats.connect({ servers: natsServers });
69 | internals.nats.on('error', (err) => {
70 | console.log(err);
71 | });
72 | cb();
73 | }
74 |
75 | function genPoint (type) {
76 | return JSON.stringify({
77 | type,
78 | time: Date.now(),
79 | value: (type === 'motion') ? Math.floor(Math.random() * 2) : Math.floor(Math.random() * 100)
80 | });
81 | }
82 |
83 | function genData () {
84 | internals.nats.publish('temperature', genPoint('temperature'));
85 | internals.nats.publish('humidity', genPoint('humidity'));
86 | internals.nats.publish('motion', genPoint('motion'));
87 | }
88 |
89 | Piloted.on('refresh', () => {
90 | setupNats();
91 | });
92 |
93 | setupSeneca();
94 | setupNats(function () {
95 | if (process.env.FAKE_MODE) {
96 | setInterval(genData, 1000);
97 | }
98 | });
99 |
--------------------------------------------------------------------------------
/smartthings/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "smartthings-server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "lib/index.js",
6 | "author": "",
7 | "license": "MPL-2.0",
8 | "dependencies": {
9 | "nats": "0.7.x",
10 | "piloted": "3.x.x",
11 | "seneca": "3.x.x"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/smartthings/sensor.groovy:
--------------------------------------------------------------------------------
1 | definition(
2 | name: "Sensor Reporter",
3 | namespace: "demo.sensor",
4 | author: "Demo",
5 | description: "Send sensor readings some place",
6 | category: "SmartThings Labs",
7 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
8 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
9 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
10 |
11 |
12 | preferences {
13 | section("Select sensor"){
14 | input "sensor", "capability.temperatureMeasurement", title: "Sensor"
15 | }
16 | section("Set reporting URL"){
17 | input "reportURL", "string", title: "Report URL"
18 | }
19 | section("Set reporting passcode"){
20 | input "reportPasscode", "string", title: "Report Passcode"
21 | }
22 | }
23 |
24 | def installed() {
25 | log.debug "Installed with settings: ${settings}"
26 |
27 | initialize()
28 | }
29 |
30 | def updated() {
31 | log.debug "Updated with settings: ${settings}"
32 |
33 | unsubscribe()
34 | initialize()
35 | }
36 |
37 | def initialize() {
38 | subscribe(sensor, "humidity", humidityHandler)
39 | subscribe(sensor, "motion.active", motionActiveHandler)
40 | subscribe(sensor, "motion.inactive", motionInactiveHandler)
41 | subscribe(sensor, "temperature", temperatureHandler)
42 | }
43 |
44 | def humidityHandler(evt)
45 | {
46 | reportHumidity(evt.doubleValue)
47 | }
48 |
49 | def motionActiveHandler(evt)
50 | {
51 | log.debug evt.value
52 | reportActiveMotion()
53 | }
54 |
55 | def motionInactiveHandler(evt)
56 | {
57 | log.debug evt.value
58 | reportInactiveMotion()
59 | }
60 |
61 | def temperatureHandler(evt)
62 | {
63 | reportTemperature(evt.doubleValue)
64 | }
65 |
66 | def reportHumidity(humidity)
67 | {
68 | def params = [
69 | role: "smartthings",
70 | cmd: "write",
71 | type: "humidity",
72 | value: humidity,
73 | passcode: reportPasscode
74 | ]
75 |
76 | try {
77 | httpPostJson(reportURL, params) { resp ->
78 | resp.headers.each {
79 | log.debug "${it.name} : ${it.value}"
80 | }
81 | log.debug "response contentType: ${resp.contentType}"
82 | log.debug "response data: ${resp.data}"
83 | }
84 | } catch (e) {
85 | log.error "something went wrong sending report: $e"
86 | }
87 | }
88 |
89 | def reportActiveMotion()
90 | {
91 | def params = [
92 | role: "smartthings",
93 | cmd: "write",
94 | type: "motion",
95 | value: 1,
96 | passcode: reportPasscode
97 | ]
98 |
99 | try {
100 | httpPostJson(reportURL, params) { resp ->
101 | resp.headers.each {
102 | log.debug "${it.name} : ${it.value}"
103 | }
104 | log.debug "response contentType: ${resp.contentType}"
105 | log.debug "response data: ${resp.data}"
106 | }
107 | } catch (e) {
108 | log.error "something went wrong sending report: $e"
109 | }
110 | }
111 |
112 | def reportInactiveMotion()
113 | {
114 | def params = [
115 | role: "smartthings",
116 | cmd: "write",
117 | type: "motion",
118 | value: 0,
119 | passcode: reportPasscode
120 | ]
121 |
122 | try {
123 | httpPostJson(reportURL, params) { resp ->
124 | resp.headers.each {
125 | log.debug "${it.name} : ${it.value}"
126 | }
127 | log.debug "response contentType: ${resp.contentType}"
128 | log.debug "response data: ${resp.data}"
129 | }
130 | } catch (e) {
131 | log.error "something went wrong sending report: $e"
132 | }
133 | }
134 |
135 | def reportTemperature(temperature)
136 | {
137 | def params = [
138 | role: "smartthings",
139 | cmd: "write",
140 | type: "temperature",
141 | value: temperature,
142 | passcode: reportPasscode
143 | ]
144 |
145 | try {
146 | httpPostJson(reportURL, params) { resp ->
147 | resp.headers.each {
148 | log.debug "${it.name} : ${it.value}"
149 | }
150 | log.debug "response contentType: ${resp.contentType}"
151 | log.debug "response data: ${resp.data}"
152 | }
153 | } catch (e) {
154 | log.error "something went wrong sending report: $e"
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/traefik/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM traefik:1.4-alpine
2 |
3 | RUN apk update && \
4 | apk add curl unzip tar && \
5 | rm -rf /var/cache/apk/*
6 |
7 | # Install Consul Agent
8 | RUN export CONSUL_VERSION=0.7.0 \
9 | && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \
10 | && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \
11 | && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \
12 | && unzip /tmp/consul -d /usr/local/bin \
13 | && rm /tmp/consul.zip \
14 | && mkdir /config
15 |
16 | # Install ContainerPilot
17 | ENV CONTAINERPILOT_VERSION 3.4.3
18 | RUN export CP_SHA1=e8258ed166bcb3de3e06638936dcc2cae32c7c58 \
19 | && curl -Lso /tmp/containerpilot.tar.gz \
20 | "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \
21 | && echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \
22 | && tar zxf /tmp/containerpilot.tar.gz -C /bin \
23 | && rm /tmp/containerpilot.tar.gz
24 |
25 | # Copy the configurations
26 | COPY traefik.toml /etc/traefik/traefik.toml
27 | ENV CONTAINERPILOT_PATH=/etc/containerpilot.json5
28 | COPY containerpilot.json5 ${CONTAINERPILOT_PATH}
29 | ENV CONTAINERPILOT=${CONTAINERPILOT_PATH}
30 |
31 | ENTRYPOINT []
32 | CMD ["/bin/containerpilot"]
33 |
--------------------------------------------------------------------------------
/traefik/containerpilot.json5:
--------------------------------------------------------------------------------
1 | {
2 | consul: 'localhost:8500',
3 | jobs: [
4 | {
5 | name: 'traefik',
6 | exec: 'traefik',
7 | port: 8080,
8 | health: {
9 | exec: '/usr/bin/curl -o /dev/null --fail -s http://localhost:8080/',
10 | interval: 2,
11 | ttl: 5
12 | },
13 | restarts: 'unlimited'
14 | },
15 | {
16 | name: 'consul-agent',
17 | exec: ['/usr/local/bin/consul', 'agent',
18 | '-data-dir=/data',
19 | '-config-dir=/config',
20 | '-log-level=err',
21 | '-rejoin',
22 | '-retry-join', '{{ .CONSUL | default "consul" }}',
23 | '-retry-max', '10',
24 | '-retry-interval', '10s'],
25 | restarts: 'unlimited'
26 | }
27 | ],
28 | telemetry: {
29 | port: 9090,
30 | interfaces: ['eth1', 'eth0', 'eth0[1]', 'lo', 'lo0', 'inet']
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/traefik/traefik.toml:
--------------------------------------------------------------------------------
1 | ################################################################
2 | # Consul Catalog configuration backend
3 | ################################################################
4 |
5 | # Enable Consul Catalog configuration backend
6 | #
7 | # Optional
8 | #
9 | [consulCatalog]
10 |
11 | # Consul server endpoint
12 | #
13 | # Required
14 | #
15 | endpoint = "localhost:8500"
16 |
17 | # Prefix for Consul catalog tags
18 | #
19 | # Optional
20 | #
21 | prefix = "traefik"
22 |
23 | [web]
24 | address = ":8080"
25 |
--------------------------------------------------------------------------------