├── .gitignore ├── README.md ├── firebase-logging.js ├── package-lock.json ├── package.json └── utils ├── color.js └── date.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | firebase-debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt Firebase Logging Logo][logo] 2 | 3 | [logo]: https://firerun.io/wp-content/uploads/2020/05/logging-logo-full.png 4 | 5 | # Easier Logging for Firebase 6 | 7 | Firebase Logging is a free open source tool that we built for the Firebase community. We are regular users of Firebase and this solves a real "logging" need for us. We hope it helps you too. 8 | 9 | See our [Firebase Logging Page](https://firerun.io/firebase-logging/) for more information. 10 | 11 | ## Why Firebase Logging 12 | 13 | Firebase's CLI (command line interface) [logging](https://firebase.google.com/docs/functions/writing-and-viewing-logs) requires you to re-run the tool just to see new data. That becomes a lot of up 'arrow key' and 'enter' pressing. 14 | 15 | **Firebase Logging** gives you mostly real-time logging by continually running the Firebase CLI logging command and printing to the console new entires. You can output the logs to the console and a file. 16 | 17 | Log enhancements include: 18 | 19 | - Colored Logs: *timestamp* blue, *function* name green, *warning* yellow, and *error* red. Default. 20 | - Locally Formatted Timestamp: format from UTC timestamp to your local time. Use `--ft` parameter to format. 21 | 22 | ### Things to Note 23 | 24 | The default polling occurs every *2 seconds*. You can set the frequency of polling with the `-freq` parameter from a minimum of 1500 milliseconds, which is the most Firebase will allow, to an infinite maximum. You might want to increase the frequency if you find the polling is taking too much system resources. 25 | 26 | The default pull is 250 lines, so if you have more than 250 lines logs per the *polling frequency*, some logs will be missed. Increase the number of log lines pulled with the `--n` parameter to capture more logs (1-1000). 27 | 28 | ## Installation 29 | 30 | `npm i -g firebase-logging` 31 | 32 | ## Requirements 33 | 34 | - [firebase-tools](https://www.npmjs.com/package/firebase-tools) globally installed. 35 | - A Firebase project id you want to see the logs of 36 | 37 | ## Usage 38 | 39 | ### Run 40 | 41 | `firebase-logging --project=[Firebase projectId]` 42 | 43 | ### Example 44 | 45 | Start the logging to the console for project myFirerun, only for the sendReport function, save to the savelog.txt file, and format the time to local. 46 | 47 | `firebase-logging --project=myFirerun --func=sendReport --file=savelog.txt --ft` 48 | 49 | ### Parameters 50 | 51 | #### Required 52 | 53 | | Parameter | Description | 54 | | ------------- |:-------------------------------------------------:| 55 | | --project | [projectId] of Firebase project | 56 | 57 | #### Optional 58 | 59 | | Parameter | Description | 60 | | ------------- |:-------------------------------------------------:| 61 | | --h | Help info | 62 | | --file | [File Name] to write the logs | 63 | | --colorOff | Turn off coloring of logs | 64 | | --func | [Function Name] of specific function to view logs | 65 | | --n | [Number] of log lines to pull (1-1000) | 66 | | --ft | Format timestamp to local date and time | 67 | | --freq | [Milliseconds] to poll for new logs | 68 | -------------------------------------------------------------------------------- /firebase-logging.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const cmd = require("node-cmd"); 4 | const fs = require("fs"); 5 | const argv = require("yargs").argv; 6 | const { formatDate } = require("./utils/date"); 7 | const { colorMe } = require("./utils/color"); 8 | 9 | const SCHEDULE = 2000; // milliseconds 10 | const DEFAULT_LINES = 250; // lines to poll 11 | 12 | let last = []; 13 | let stream; 14 | const project = argv.project ? `--project ${argv.project}` : null; 15 | const only = argv.function ? `--only ${argv.func}` : ""; 16 | const lines = argv.n ? argv.n : DEFAULT_LINES; 17 | const freq = !isNaN(argv.freq) ? parseInt(argv.freq) : SCHEDULE; 18 | 19 | if (argv.h || argv.help) { 20 | console.log( 21 | "Usage: firebase-logging\n\nRequired:\n--project=[projectId] : Firebase Project Id\n\nOptional:\n--n=[number] : Number of log line to poll, default 250\n--file=[File Name] : File to write logs\n--func=[Function Name] : View specific function logs\n--colorOff : Turn off coloring of logs\n--ft : Format timestamp to local\n--freq=[milliseconds] : polling frequency, default 2000" 22 | ); 23 | 24 | process.exit(); 25 | } 26 | 27 | if(!project) { 28 | console.error("You need to provide a Firebase project with the '--project=[projectId]' parameter."); 29 | process.exit(); 30 | } 31 | else if (freq < 1500) { 32 | console.error("Minimum frequency is 1500 milliseconds."); 33 | process.exit(); 34 | } else { 35 | console.log( 36 | `Starting firebase-logging, polling every ${freq} milliseconds ...` 37 | ); 38 | } 39 | 40 | if (argv.file) { 41 | stream = fs.createWriteStream(argv.file, { flags: "a" }); 42 | } 43 | 44 | const getLogs = () => { 45 | cmd.get( 46 | `firebase ${project} functions:log -n ${lines} ${only}`, 47 | (err, data, stderr) => { 48 | if (err) { 49 | console.error(err); 50 | } 51 | 52 | const splitData = data.trim().split("\n"); 53 | let diff = splitData.filter((x) => !last.includes(x)); 54 | 55 | if (diff.length > 0) { 56 | if (!argv.colorOff || argv.ft) { 57 | diff = diff.map((x) => { 58 | let logArr = x.split(" "); 59 | 60 | // Format time 61 | if (argv.ft) { 62 | logArr[0] = formatDate(logArr[0]); 63 | } 64 | 65 | // Add some color 66 | if (!argv.colorOff) { 67 | logArr = colorMe(logArr); 68 | } 69 | 70 | return logArr.join(" "); 71 | }); 72 | } 73 | 74 | const strLogs = diff.join("\n"); 75 | console.log(strLogs); 76 | 77 | argv.file && stream.write(strLogs); 78 | } 79 | 80 | last = [...splitData]; 81 | } 82 | ); 83 | }; 84 | 85 | getLogs(); 86 | setInterval(() => { 87 | getLogs(); 88 | }, freq); 89 | 90 | process.on("SIGINT", () => { 91 | console.log("\nStopping firebase-logging"); 92 | 93 | argv.file && stream.end(); 94 | process.exit(); 95 | }); 96 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase-logging", 3 | "version": "1.0.6", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/color-name": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", 10 | "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" 11 | }, 12 | "ansi-regex": { 13 | "version": "5.0.0", 14 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 15 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" 16 | }, 17 | "ansi-styles": { 18 | "version": "4.2.1", 19 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", 20 | "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", 21 | "requires": { 22 | "@types/color-name": "^1.1.1", 23 | "color-convert": "^2.0.1" 24 | } 25 | }, 26 | "camelcase": { 27 | "version": "5.3.1", 28 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 29 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" 30 | }, 31 | "chalk": { 32 | "version": "4.0.0", 33 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", 34 | "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", 35 | "requires": { 36 | "ansi-styles": "^4.1.0", 37 | "supports-color": "^7.1.0" 38 | } 39 | }, 40 | "cliui": { 41 | "version": "6.0.0", 42 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", 43 | "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", 44 | "requires": { 45 | "string-width": "^4.2.0", 46 | "strip-ansi": "^6.0.0", 47 | "wrap-ansi": "^6.2.0" 48 | } 49 | }, 50 | "color-convert": { 51 | "version": "2.0.1", 52 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 53 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 54 | "requires": { 55 | "color-name": "~1.1.4" 56 | } 57 | }, 58 | "color-name": { 59 | "version": "1.1.4", 60 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 61 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 62 | }, 63 | "decamelize": { 64 | "version": "1.2.0", 65 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 66 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" 67 | }, 68 | "emoji-regex": { 69 | "version": "8.0.0", 70 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 71 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 72 | }, 73 | "find-up": { 74 | "version": "4.1.0", 75 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 76 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 77 | "requires": { 78 | "locate-path": "^5.0.0", 79 | "path-exists": "^4.0.0" 80 | } 81 | }, 82 | "get-caller-file": { 83 | "version": "2.0.5", 84 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 85 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 86 | }, 87 | "has-flag": { 88 | "version": "4.0.0", 89 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 90 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 91 | }, 92 | "is-fullwidth-code-point": { 93 | "version": "3.0.0", 94 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 95 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 96 | }, 97 | "locate-path": { 98 | "version": "5.0.0", 99 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 100 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 101 | "requires": { 102 | "p-locate": "^4.1.0" 103 | } 104 | }, 105 | "node-cmd": { 106 | "version": "3.0.0", 107 | "resolved": "https://registry.npmjs.org/node-cmd/-/node-cmd-3.0.0.tgz", 108 | "integrity": "sha1-OP/3CkqqT2WdID61eGJzcBjiT28=" 109 | }, 110 | "p-limit": { 111 | "version": "2.3.0", 112 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 113 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 114 | "requires": { 115 | "p-try": "^2.0.0" 116 | } 117 | }, 118 | "p-locate": { 119 | "version": "4.1.0", 120 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 121 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 122 | "requires": { 123 | "p-limit": "^2.2.0" 124 | } 125 | }, 126 | "p-try": { 127 | "version": "2.2.0", 128 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 129 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" 130 | }, 131 | "path-exists": { 132 | "version": "4.0.0", 133 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 134 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" 135 | }, 136 | "require-directory": { 137 | "version": "2.1.1", 138 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 139 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" 140 | }, 141 | "require-main-filename": { 142 | "version": "2.0.0", 143 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", 144 | "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" 145 | }, 146 | "set-blocking": { 147 | "version": "2.0.0", 148 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 149 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 150 | }, 151 | "string-width": { 152 | "version": "4.2.0", 153 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 154 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 155 | "requires": { 156 | "emoji-regex": "^8.0.0", 157 | "is-fullwidth-code-point": "^3.0.0", 158 | "strip-ansi": "^6.0.0" 159 | } 160 | }, 161 | "strip-ansi": { 162 | "version": "6.0.0", 163 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 164 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 165 | "requires": { 166 | "ansi-regex": "^5.0.0" 167 | } 168 | }, 169 | "supports-color": { 170 | "version": "7.1.0", 171 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", 172 | "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", 173 | "requires": { 174 | "has-flag": "^4.0.0" 175 | } 176 | }, 177 | "which-module": { 178 | "version": "2.0.0", 179 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", 180 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" 181 | }, 182 | "wrap-ansi": { 183 | "version": "6.2.0", 184 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", 185 | "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", 186 | "requires": { 187 | "ansi-styles": "^4.0.0", 188 | "string-width": "^4.1.0", 189 | "strip-ansi": "^6.0.0" 190 | } 191 | }, 192 | "y18n": { 193 | "version": "4.0.0", 194 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", 195 | "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" 196 | }, 197 | "yargs": { 198 | "version": "15.3.1", 199 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", 200 | "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", 201 | "requires": { 202 | "cliui": "^6.0.0", 203 | "decamelize": "^1.2.0", 204 | "find-up": "^4.1.0", 205 | "get-caller-file": "^2.0.1", 206 | "require-directory": "^2.1.1", 207 | "require-main-filename": "^2.0.0", 208 | "set-blocking": "^2.0.0", 209 | "string-width": "^4.2.0", 210 | "which-module": "^2.0.0", 211 | "y18n": "^4.0.0", 212 | "yargs-parser": "^18.1.1" 213 | } 214 | }, 215 | "yargs-parser": { 216 | "version": "18.1.3", 217 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", 218 | "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", 219 | "requires": { 220 | "camelcase": "^5.0.0", 221 | "decamelize": "^1.2.0" 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase-logging", 3 | "version": "1.0.16", 4 | "description": "Real-time logging for Firebase", 5 | "main": "firebase-logging.js", 6 | "scripts": { 7 | "start": "node firebase-logging.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/fireRun-io/firebase-logging.git" 12 | }, 13 | "keywords": [ 14 | "firebase", 15 | "realtime", 16 | "logs", 17 | "logging", 18 | "functions", 19 | "google", 20 | "cloud" 21 | ], 22 | "author": "Geoffrey Bourne", 23 | "url": "https://www.anothermadworld.com", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/fireRun-io/firebase-logging/issues" 27 | }, 28 | "bin": { 29 | "firebase-logging": "./firebase-logging.js" 30 | }, 31 | "homepage": "https://firerun.io/firebase-logging", 32 | "dependencies": { 33 | "chalk": "^4.0.0", 34 | "node-cmd": "^3.0.0", 35 | "yargs": "^15.3.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /utils/color.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | 3 | const colorMe = (logArr) => { 4 | logArr[0] = chalk.blue(logArr[0]); 5 | logArr[1] = 6 | logArr[1] === "E" 7 | ? chalk.red.bold(logArr[1]) 8 | : logArr[1] === "W" 9 | ? chalk.yellow.bold(logArr[1]) 10 | : logArr[1]; 11 | logArr[2] = chalk.green(logArr[2]); 12 | 13 | const coloredLogs = logArr.map((y) => { 14 | if (y) { 15 | const lower = y.toLowerCase(); 16 | if (lower.includes("error")) { 17 | y = chalk.red.bold(y); 18 | } else if (lower.includes("warning")) { 19 | y = chalk.yellow.bold(y); 20 | } 21 | } 22 | 23 | return y; 24 | }); 25 | 26 | return coloredLogs; 27 | }; 28 | 29 | exports.colorMe = colorMe; 30 | -------------------------------------------------------------------------------- /utils/date.js: -------------------------------------------------------------------------------- 1 | const isValidDate = (date) => { 2 | return date instanceof Date && !isNaN(date); 3 | }; 4 | 5 | const formatLocalDate = (date) => { 6 | let hours = date.getHours(); 7 | let minutes = date.getMinutes(); 8 | let amPm = hours >= 12 ? "pm" : "am"; 9 | 10 | hours = hours % 12; 11 | hours = hours ? hours : 12; 12 | minutes = minutes < 10 ? "0" + minutes : minutes; 13 | 14 | const strTime = hours + ":" + minutes + " " + amPm; 15 | return ( 16 | date.getMonth() + 17 | 1 + 18 | "/" + 19 | date.getDate() + 20 | "/" + 21 | date.getFullYear() + 22 | " " + 23 | strTime 24 | ); 25 | }; 26 | 27 | const formatDate = (str) => { 28 | const tmpDate = new Date(str); 29 | const formattedStr = isValidDate(tmpDate) ? formatLocalDate(tmpDate) : str; 30 | 31 | return formattedStr; 32 | }; 33 | 34 | exports.formatDate = formatDate; 35 | --------------------------------------------------------------------------------