diff --git a/about.html b/about.html new file mode 100644 index 000000000..ba6ae4863 --- /dev/null +++ b/about.html @@ -0,0 +1,171 @@ + + + + + + About — logback-android + + + + + + + + + +
+ + +
+

What is it?

+

+ logback-android is a lite version of logback + that runs on Android. logback is the successor to the classic log4j project, designed by the same + author, and is one of the most widely used logging frameworks on the JVM. logback-android adapts + that engine for Android: it removes server-oriented components that don't make sense on a phone + (Groovy config, JMX, servlet integration) and adds Android-specific ones, like the + LogcatAppender and runtime variables for app data directories. +

+

+ Your application code never talks to logback-android directly. Instead, it logs through + SLF4J, the standard Java logging facade. logback-android + plugs in underneath as the implementation, which means libraries written against SLF4J — + the vast majority of Java logging code — work on Android without modification. +

+ +

Design goals

+ + +

A short history

+

+ The project was started in 2011 by Anthony Trinh (@tony19) + as a port of logback to Android, and has been maintained continuously since. The 1.x series tracked + logback 1.1; the 2.x series moved to logback 1.2 with AndroidX-era tooling; and the current 3.x + series modernizes the toolchain with JDK-specific artifacts: +

+
+ + + + + + + + +
ArtifactBytecodeminSdk
com.github.tony19:logback-androidJava 1126 (Android 8.0)
com.github.tony19:logback-android-jdk8Java 821 (Android 5.0)
+
+

+ Use logback-android unless your app must support devices older than Android 8.0, + in which case use logback-android-jdk8. For the legacy 1.x line, see the + 1.x branch. +

+ +

How the pieces fit

+
┌─────────────────────────────┐
+│    Your app & libraries     │
+│   log.info("hello world")   │
+└──────────────┬──────────────┘
+               │  SLF4J API (facade)
+┌──────────────▼──────────────┐
+│       logback-android       │
+│  levels · filters · layout  │
+└──┬─────┬─────┬─────┬─────┬──┘
+   │     │     │     │     │
+logcat  file SQLite socket syslog/email
+ +

License & contributing

+

+ logback-android is free software released under the + Apache License 2.0. + Bug reports, feature requests, and pull requests are welcome on the + GitHub repository. +

+
+
+ + + + + diff --git a/assets/css/site.css b/assets/css/site.css new file mode 100644 index 000000000..2d61037c1 --- /dev/null +++ b/assets/css/site.css @@ -0,0 +1,619 @@ +/* logback-android site styles */ + +:root { + --bg: #f7f8fa; + --bg-alt: #ffffff; + --bg-inset: #eef1f5; + --text: #1b2430; + --text-muted: #5a6676; + --border: #dde3ea; + --accent: #2f7d4f; + --accent-strong: #256640; + --accent-soft: #e3f2e9; + --accent-2: #1f6feb; + --code-bg: #0f172a; + --code-text: #e2e8f0; + --danger: #c0392b; + --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 8px 24px rgba(16, 24, 40, 0.08); + --radius: 12px; + --maxw: 1080px; + color-scheme: light; +} + +:root[data-theme="dark"] { + --bg: #0d1117; + --bg-alt: #161b22; + --bg-inset: #1c2330; + --text: #e6edf3; + --text-muted: #9aa7b4; + --border: #2d3646; + --accent: #3fb950; + --accent-strong: #6ee7a0; + --accent-soft: #12301f; + --accent-2: #58a6ff; + --code-bg: #10161f; + --code-text: #dbe4ee; + --danger: #ff7b72; + --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35); + color-scheme: dark; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 1.6; + color: var(--text); + background: var(--bg); + -webkit-font-smoothing: antialiased; +} + +a { color: var(--accent-2); text-decoration: none; } +a:hover { text-decoration: underline; } + +code, pre, .mono { + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; +} + +code { + background: var(--bg-inset); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.1em 0.35em; + font-size: 0.875em; +} + +pre { + background: var(--code-bg); + color: var(--code-text); + border-radius: var(--radius); + padding: 1rem 1.25rem; + overflow-x: auto; + font-size: 0.85rem; + line-height: 1.55; + position: relative; +} + +pre code { + background: none; + border: none; + padding: 0; + font-size: inherit; + color: inherit; +} + +.container { + max-width: var(--maxw); + margin: 0 auto; + padding: 0 1.25rem; +} + +/* ---------- Header / nav ---------- */ + +.site-header { + position: sticky; + top: 0; + z-index: 50; + background: color-mix(in srgb, var(--bg-alt) 88%, transparent); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border); +} + +.nav { + display: flex; + align-items: center; + gap: 1rem; + height: 60px; +} + +.brand { + display: flex; + align-items: center; + gap: 0.55rem; + font-weight: 700; + font-size: 1.05rem; + color: var(--text); + white-space: nowrap; +} +.brand:hover { text-decoration: none; } + +.brand svg { flex: none; } + +.nav-links { + display: flex; + align-items: center; + gap: 0.25rem; + margin-left: auto; +} + +.nav-links a { + color: var(--text-muted); + font-weight: 500; + font-size: 0.925rem; + padding: 0.4rem 0.7rem; + border-radius: 8px; +} + +.nav-links a:hover { + color: var(--text); + background: var(--bg-inset); + text-decoration: none; +} + +.nav-links a[aria-current="page"] { + color: var(--accent); + font-weight: 600; +} + +.nav-cta { + display: inline-flex; + align-items: center; + gap: 0.4rem; + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.4rem 0.75rem !important; + color: var(--text) !important; +} + +.icon-btn { + border: 1px solid var(--border); + background: transparent; + color: var(--text-muted); + border-radius: 8px; + width: 36px; + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} +.icon-btn:hover { color: var(--text); background: var(--bg-inset); } + +.nav-toggle { display: none; } + +@media (max-width: 780px) { + .nav-toggle { display: inline-flex; } + .nav-links { + display: none; + position: absolute; + top: 60px; + left: 0; + right: 0; + flex-direction: column; + align-items: stretch; + background: var(--bg-alt); + border-bottom: 1px solid var(--border); + padding: 0.5rem 1rem 1rem; + box-shadow: var(--shadow); + } + .nav-links.open { display: flex; } +} + +/* ---------- Hero ---------- */ + +.hero { + padding: 4.5rem 0 3.5rem; + text-align: center; + background: + radial-gradient(60rem 24rem at 50% -8rem, var(--accent-soft), transparent 70%); +} + +.hero h1 { + font-size: clamp(2.1rem, 5vw, 3.25rem); + line-height: 1.15; + margin: 0 0 1rem; + letter-spacing: -0.02em; +} + +.hero h1 .accent { color: var(--accent); } + +.hero .tagline { + max-width: 640px; + margin: 0 auto 2rem; + color: var(--text-muted); + font-size: 1.15rem; +} + +.badges { + display: flex; + justify-content: center; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 1.5rem; +} + +.badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.8rem; + font-weight: 600; + color: var(--accent); + background: var(--accent-soft); + border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); + border-radius: 999px; + padding: 0.25rem 0.75rem; +} + +.hero-actions { + display: flex; + justify-content: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + font-size: 0.95rem; + border-radius: 10px; + padding: 0.65rem 1.25rem; + border: 1px solid transparent; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.btn-primary { + background: var(--accent); + color: #fff !important; +} +.btn-primary:hover { background: var(--accent-strong); text-decoration: none; } +:root[data-theme="dark"] .btn-primary { color: #062514 !important; } + +.btn-secondary { + background: var(--bg-alt); + border-color: var(--border); + color: var(--text) !important; +} +.btn-secondary:hover { background: var(--bg-inset); text-decoration: none; } + +.hero-code { + max-width: 720px; + margin: 2.5rem auto 0; + text-align: left; +} + +/* ---------- Sections ---------- */ + +.section { padding: 3.5rem 0; } +.section.alt { background: var(--bg-alt); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); } + +.section h2 { + font-size: 1.75rem; + letter-spacing: -0.01em; + margin: 0 0 0.5rem; +} + +.section > .container > .lead, +.lead { + color: var(--text-muted); + max-width: 680px; + margin-top: 0; +} + +.grid { + display: grid; + gap: 1.25rem; + margin-top: 2rem; +} +.grid.cols-3 { grid-template-columns: repeat(3, 1fr); } +.grid.cols-2 { grid-template-columns: repeat(2, 1fr); } + +@media (max-width: 900px) { .grid.cols-3 { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 620px) { .grid.cols-3, .grid.cols-2 { grid-template-columns: 1fr; } } + +.card { + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.4rem; +} +.section.alt .card { background: var(--bg); } + +.card .card-icon { + width: 42px; + height: 42px; + border-radius: 10px; + background: var(--accent-soft); + color: var(--accent); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.9rem; +} + +.card h3 { margin: 0 0 0.4rem; font-size: 1.05rem; } +.card p { margin: 0; color: var(--text-muted); font-size: 0.925rem; } + +.steps { counter-reset: step; list-style: none; margin: 2rem 0 0; padding: 0; } +.steps > li { + counter-increment: step; + position: relative; + padding: 0 0 2rem 3.25rem; +} +.steps > li:last-child { padding-bottom: 0; } +.steps > li::before { + content: counter(step); + position: absolute; + left: 0; + top: 0; + width: 34px; + height: 34px; + border-radius: 50%; + background: var(--accent); + color: #fff; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.9rem; +} +:root[data-theme="dark"] .steps > li::before { color: #062514; } +.steps > li:not(:last-child)::after { + content: ""; + position: absolute; + left: 16px; + top: 40px; + bottom: 6px; + width: 2px; + background: var(--border); +} +.steps h3 { margin: 0.25rem 0 0.5rem; font-size: 1.05rem; } +.steps p { color: var(--text-muted); margin: 0 0 0.75rem; } + +/* copy button on code blocks */ +.copy-btn { + position: absolute; + top: 0.6rem; + right: 0.6rem; + border: 1px solid rgba(148, 163, 184, 0.35); + background: rgba(148, 163, 184, 0.12); + color: #cbd5e1; + font-size: 0.75rem; + font-weight: 600; + border-radius: 6px; + padding: 0.25rem 0.6rem; + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease; +} +pre:hover .copy-btn, .copy-btn:focus-visible { opacity: 1; } +.copy-btn:hover { background: rgba(148, 163, 184, 0.25); } + +/* tables */ +.table-wrap { overflow-x: auto; margin-top: 1.5rem; } +table.nice { + width: 100%; + border-collapse: collapse; + font-size: 0.925rem; + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +table.nice th, table.nice td { + text-align: left; + padding: 0.65rem 1rem; + border-bottom: 1px solid var(--border); +} +table.nice th { background: var(--bg-inset); font-weight: 600; } +table.nice tr:last-child td { border-bottom: none; } + +/* prose pages */ +.prose { max-width: 760px; } +.prose h2 { margin-top: 2.5rem; } +.prose h3 { margin-top: 1.75rem; } +.prose p, .prose li { color: var(--text); } +.prose .muted { color: var(--text-muted); } + +.page-header { padding: 3rem 0 0.5rem; } +.page-header h1 { font-size: 2.25rem; letter-spacing: -0.02em; margin: 0 0 0.5rem; } +.page-header p { color: var(--text-muted); font-size: 1.1rem; max-width: 680px; margin: 0; } + +/* callouts */ +.callout { + border: 1px solid var(--border); + border-left: 4px solid var(--accent); + background: var(--bg-alt); + border-radius: 8px; + padding: 0.9rem 1.1rem; + margin: 1.25rem 0; + font-size: 0.925rem; +} +.callout.info { border-left-color: var(--accent-2); } + +/* ---------- Footer ---------- */ + +.site-footer { + border-top: 1px solid var(--border); + padding: 2.5rem 0; + margin-top: 4rem; + color: var(--text-muted); + font-size: 0.9rem; +} + +.footer-grid { + display: flex; + justify-content: space-between; + gap: 2rem; + flex-wrap: wrap; +} + +.footer-col h4 { margin: 0 0 0.6rem; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text); } +.footer-col ul { list-style: none; margin: 0; padding: 0; } +.footer-col li { margin-bottom: 0.4rem; } +.footer-col a { color: var(--text-muted); } +.footer-col a:hover { color: var(--text); } + +.footer-note { margin-top: 2rem; padding-top: 1.25rem; border-top: 1px solid var(--border); } + +/* ---------- Config editor ---------- */ + +.editor-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 1.5rem; + align-items: start; + margin: 2rem 0 3rem; +} + +@media (max-width: 980px) { + .editor-layout { grid-template-columns: 1fr; } +} + +.panel { + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} + +.panel-header { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.8rem 1.1rem; + border-bottom: 1px solid var(--border); + background: var(--bg-inset); + font-weight: 600; + font-size: 0.95rem; +} +.panel-header .spacer { margin-left: auto; } +.panel-body { padding: 1.1rem; } + +.fieldset { + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.9rem 1rem 1rem; + margin-bottom: 1rem; + background: var(--bg); +} +.fieldset:last-child { margin-bottom: 0; } + +.fieldset-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; +} +.fieldset-header h4 { margin: 0; font-size: 0.95rem; } +.fieldset-header .tag { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + background: var(--accent-soft); + color: var(--accent); + border-radius: 999px; + padding: 0.1rem 0.55rem; +} +.fieldset-header .remove { margin-left: auto; } + +.form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 0.75rem; + margin-bottom: 0.75rem; +} +.form-row:last-child { margin-bottom: 0; } + +.field { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; } +.field.wide { grid-column: 1 / -1; } + +.field label { + font-size: 0.78rem; + font-weight: 600; + color: var(--text-muted); +} + +.field input[type="text"], +.field input[type="number"], +.field select { + width: 100%; + font: inherit; + font-size: 0.9rem; + color: var(--text); + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.45rem 0.6rem; +} +.field input:focus, .field select:focus { + outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent); + outline-offset: 0; + border-color: var(--accent); +} + +.check { + display: inline-flex; + align-items: center; + gap: 0.45rem; + font-size: 0.875rem; + color: var(--text); + cursor: pointer; + user-select: none; +} +.check input { accent-color: var(--accent); width: 16px; height: 16px; } + +.btn-sm { + font-size: 0.82rem; + padding: 0.35rem 0.75rem; + border-radius: 8px; +} + +.btn-ghost { + background: transparent; + border: 1px dashed var(--border); + color: var(--text-muted); + width: 100%; + justify-content: center; +} +.btn-ghost:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); } + +.btn-danger-ghost { + background: transparent; + border: none; + color: var(--text-muted); + padding: 0.25rem 0.5rem; + font-size: 0.8rem; +} +.btn-danger-ghost:hover { color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent); } + +.empty-hint { + color: var(--text-muted); + font-size: 0.875rem; + text-align: center; + padding: 0.75rem 0; +} + +/* xml preview */ +.xml-preview { + margin: 0; + border-radius: 0; + min-height: 300px; + max-height: 75vh; + overflow: auto; + font-size: 0.8rem; +} + +.xml-preview .tag-name { color: #7ee787; } +.xml-preview .attr-name { color: #79c0ff; } +.xml-preview .attr-value { color: #a5d6ff; } +.xml-preview .xml-comment { color: #8b949e; font-style: italic; } +.xml-preview .xml-text { color: #ffa657; } + +.editor-sticky { position: sticky; top: 76px; } +@media (max-width: 980px) { .editor-sticky { position: static; } } + +.toolbar-note { + font-size: 0.8rem; + color: var(--text-muted); + padding: 0.6rem 1.1rem; + border-top: 1px solid var(--border); +} diff --git a/assets/img/favicon.svg b/assets/img/favicon.svg new file mode 100644 index 000000000..a85f0d77d --- /dev/null +++ b/assets/img/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/js/editor.js b/assets/js/editor.js new file mode 100644 index 000000000..88f784bb2 --- /dev/null +++ b/assets/js/editor.js @@ -0,0 +1,664 @@ +/* logback-android interactive config editor + * + * Maintains a JSON model of the configuration (settings, properties, + * appenders, loggers) and regenerates a highlighted logback.xml preview + * on every change. State persists to localStorage. + */ +(function () { + 'use strict'; + + var STORAGE_KEY = 'lba-editor-state-v1'; + var LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR']; + var FILTER_LEVELS = ['', 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR']; + var SYSLOG_FACILITIES = ['USER', 'AUTH', 'AUTHPRIV', 'DAEMON', 'SYSLOG', 'LOCAL0', 'LOCAL1', 'LOCAL2', 'LOCAL3', 'LOCAL4', 'LOCAL5', 'LOCAL6', 'LOCAL7']; + + // --------------------------------------------------------------------- + // Appender type definitions: option fields + XML generation + // --------------------------------------------------------------------- + + var APPENDER_TYPES = { + logcat: { + label: 'Logcat', + className: 'ch.qos.logback.classic.android.LogcatAppender', + defaults: { tagPattern: '%logger{12}', pattern: '[%-20thread] %msg', checkLoggable: false }, + fields: [ + { key: 'tagPattern', label: 'Tag pattern', type: 'text' }, + { key: 'pattern', label: 'Message pattern', type: 'text' }, + { key: 'checkLoggable', label: 'Honor Log.isLoggable() tag levels', type: 'bool' } + ], + body: function (o) { + var el = []; + if (o.checkLoggable) el.push(tag('checkLoggable', 'true')); + el.push(wrap('tagEncoder', [tag('pattern', o.tagPattern)])); + el.push(wrap('encoder', [tag('pattern', o.pattern)])); + return el; + } + }, + + file: { + label: 'File', + className: 'ch.qos.logback.core.FileAppender', + defaults: { file: '${LOG_DIR}/app.log', append: true, lazy: false, pattern: '%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n' }, + fields: [ + { key: 'file', label: 'File path', type: 'text' }, + { key: 'pattern', label: 'Message pattern', type: 'text' }, + { key: 'append', label: 'Append to existing file', type: 'bool' }, + { key: 'lazy', label: 'Lazy file creation (on first log)', type: 'bool' } + ], + body: function (o) { + var el = [tag('file', o.file)]; + if (!o.append) el.push(tag('append', 'false')); + if (o.lazy) el.push(tag('lazy', 'true')); + el.push(wrap('encoder', [tag('pattern', o.pattern)])); + return el; + } + }, + + rolling: { + label: 'Rolling file', + className: 'ch.qos.logback.core.rolling.RollingFileAppender', + defaults: { + file: '${LOG_DIR}/app.log', + fileNamePattern: '${LOG_DIR}/app.%d{yyyy-MM-dd}.%i.log', + maxHistory: '7', + maxFileSize: '5MB', + pattern: '%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n' + }, + fields: [ + { key: 'file', label: 'Active file path', type: 'text' }, + { key: 'fileNamePattern', label: 'Rolled file name pattern', type: 'text' }, + { key: 'maxHistory', label: 'Max history (rolled files kept)', type: 'text', size: 'small' }, + { key: 'maxFileSize', label: 'Max file size (blank = time-based only)', type: 'text', size: 'small' }, + { key: 'pattern', label: 'Message pattern', type: 'text' } + ], + body: function (o) { + var policy = [tag('fileNamePattern', o.fileNamePattern)]; + if (o.maxHistory) policy.push(tag('maxHistory', o.maxHistory)); + if (o.maxFileSize) { + policy.push(wrap('timeBasedFileNamingAndTriggeringPolicy', + [tag('maxFileSize', o.maxFileSize)], + { class: 'ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP' })); + } + return [ + tag('file', o.file), + wrap('rollingPolicy', policy, { class: 'ch.qos.logback.core.rolling.TimeBasedRollingPolicy' }), + wrap('encoder', [tag('pattern', o.pattern)]) + ]; + } + }, + + sqlite: { + label: 'SQLite', + className: 'ch.qos.logback.classic.android.SQLiteAppender', + defaults: { filename: 'logs.db', maxHistory: '7 days' }, + fields: [ + { key: 'filename', label: 'Database file name (blank = default)', type: 'text' }, + { key: 'maxHistory', label: 'Max history (e.g., "7 days")', type: 'text', size: 'small' } + ], + body: function (o) { + var el = []; + if (o.filename) el.push(tag('filename', o.filename)); + if (o.maxHistory) el.push(tag('maxHistory', o.maxHistory)); + return el; + } + }, + + socket: { + label: 'Socket', + className: 'ch.qos.logback.classic.net.SocketAppender', + defaults: { remoteHost: '192.168.1.5', port: '4560', reconnectionDelay: '10000', includeCallerData: false }, + fields: [ + { key: 'remoteHost', label: 'Remote host', type: 'text' }, + { key: 'port', label: 'Port', type: 'text', size: 'small' }, + { key: 'reconnectionDelay', label: 'Reconnection delay (ms)', type: 'text', size: 'small' }, + { key: 'includeCallerData', label: 'Include caller data', type: 'bool' } + ], + body: function (o) { + var el = [tag('remoteHost', o.remoteHost), tag('port', o.port)]; + if (o.reconnectionDelay) el.push(tag('reconnectionDelay', o.reconnectionDelay)); + if (o.includeCallerData) el.push(tag('includeCallerData', 'true')); + return el; + } + }, + + syslog: { + label: 'Syslog', + className: 'ch.qos.logback.classic.net.SyslogAppender', + defaults: { syslogHost: '192.168.1.5', port: '514', facility: 'USER', suffixPattern: '[%thread] %logger %msg' }, + fields: [ + { key: 'syslogHost', label: 'Syslog host', type: 'text' }, + { key: 'port', label: 'Port', type: 'text', size: 'small' }, + { key: 'facility', label: 'Facility', type: 'select', options: SYSLOG_FACILITIES }, + { key: 'suffixPattern', label: 'Suffix pattern', type: 'text' } + ], + body: function (o) { + return [ + tag('syslogHost', o.syslogHost), + tag('port', o.port), + tag('facility', o.facility), + tag('suffixPattern', o.suffixPattern) + ]; + } + }, + + smtp: { + label: 'Email (SMTP)', + className: 'ch.qos.logback.classic.net.SMTPAppender', + defaults: { + smtpHost: 'smtp.example.com', smtpPort: '25', ssl: false, + username: '', password: '', + to: 'errors@example.com', from: 'app@example.com', + subject: '%logger{20} - %m', + pattern: '%date %-5level %logger{35} - %message%n' + }, + fields: [ + { key: 'smtpHost', label: 'SMTP host', type: 'text' }, + { key: 'smtpPort', label: 'SMTP port', type: 'text', size: 'small' }, + { key: 'ssl', label: 'Use SSL', type: 'bool' }, + { key: 'username', label: 'Username (optional)', type: 'text' }, + { key: 'password', label: 'Password (optional)', type: 'text' }, + { key: 'to', label: 'To', type: 'text' }, + { key: 'from', label: 'From', type: 'text' }, + { key: 'subject', label: 'Subject pattern', type: 'text' }, + { key: 'pattern', label: 'Message pattern', type: 'text' } + ], + body: function (o) { + var el = [tag('smtpHost', o.smtpHost), tag('smtpPort', o.smtpPort)]; + if (o.ssl) el.push(tag('SSL', 'true')); + if (o.username) el.push(tag('username', o.username)); + if (o.password) el.push(tag('password', o.password)); + el.push(tag('to', o.to), tag('from', o.from), tag('subject', o.subject)); + el.push(wrap('layout', [tag('pattern', o.pattern)], { class: 'ch.qos.logback.classic.PatternLayout' })); + return el; + } + } + }; + + // --------------------------------------------------------------------- + // State + // --------------------------------------------------------------------- + + var nextId = 1; + + function defaultState() { + return { + debug: false, + properties: [], + rootLevel: 'DEBUG', + appenders: [], + loggers: [] + }; + } + + var state = null; + + var PRESETS = { + logcat: function () { + var s = defaultState(); + s.appenders.push(makeAppenderFor(s, 'logcat')); + return s; + }, + 'logcat-file': function () { + var s = defaultState(); + s.appenders.push(makeAppenderFor(s, 'logcat')); + s.appenders.push(makeAppenderFor(s, 'rolling')); + s.loggers.push({ name: 'com.example', level: 'DEBUG', additivity: true }); + return s; + }, + remote: function () { + var s = defaultState(); + s.appenders.push(makeAppenderFor(s, 'logcat')); + var sock = makeAppenderFor(s, 'socket'); + sock.filterLevel = 'WARN'; + s.appenders.push(sock); + return s; + } + }; + + // makeAppender variant usable before `state` points at the target object + function makeAppenderFor(s, type) { + var def = APPENDER_TYPES[type]; + var names = s.appenders.map(function (a) { return a.name; }); + var name = type, n = 2; + while (names.indexOf(name) !== -1) name = type + '-' + (n++); + return { + id: nextId++, + type: type, + name: name, + attachToRoot: true, + filterLevel: '', + options: JSON.parse(JSON.stringify(def.defaults)) + }; + } + + function saveState() { + try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ } + } + + function loadState() { + try { + var raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + var s = JSON.parse(raw); + if (!s || !Array.isArray(s.appenders) || !Array.isArray(s.loggers)) return null; + // re-seed id counter past any stored ids + s.appenders.forEach(function (a) { if (a.id >= nextId) nextId = a.id + 1; }); + return s; + } catch (e) { + return null; + } + } + + // --------------------------------------------------------------------- + // XML generation + // --------------------------------------------------------------------- + + function escapeXml(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + // element helpers produce {name, attrs, children|text} + function tag(name, text) { return { name: name, text: text == null ? '' : String(text) }; } + function wrap(name, children, attrs) { return { name: name, attrs: attrs || {}, children: children }; } + + function renderEl(el, indent) { + var pad = new Array(indent + 1).join(' '); + var attrs = ''; + if (el.attrs) { + for (var k in el.attrs) { + if (el.attrs[k] !== undefined && el.attrs[k] !== '') { + attrs += ' ' + k + '="' + escapeXml(el.attrs[k]) + '"'; + } + } + } + if (el.children && el.children.length) { + var inner = el.children.map(function (c) { return renderEl(c, indent + 1); }).join('\n'); + return pad + '<' + el.name + attrs + '>\n' + inner + '\n' + pad + ''; + } + if (el.text !== undefined && el.text !== '') { + return pad + '<' + el.name + attrs + '>' + escapeXml(el.text) + ''; + } + return pad + '<' + el.name + attrs + ' />'; + } + + function generateXml() { + var lines = []; + var attrs = 'xmlns="https://tony19.github.io/logback-android/xml"\n' + + ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' + + ' xsi:schemaLocation="https://tony19.github.io/logback-android/xml https://cdn.jsdelivr.net/gh/tony19/logback-android/logback.xsd"'; + if (state.debug) attrs += '\n debug="true"'; + + lines.push(''); + + var blocks = []; + + if (state.properties.length) { + blocks.push(state.properties + .filter(function (p) { return p.key; }) + .map(function (p) { + return ' '; + }).join('\n')); + } + + state.appenders.forEach(function (a) { + var def = APPENDER_TYPES[a.type]; + var children = []; + if (a.filterLevel) { + children.push(wrap('filter', [tag('level', a.filterLevel)], + { class: 'ch.qos.logback.classic.filter.ThresholdFilter' })); + } + children = children.concat(def.body(a.options)); + blocks.push(renderEl(wrap('appender', children, { name: a.name, class: def.className }), 1)); + }); + + state.loggers.forEach(function (l) { + if (!l.name) return; + var attrsStr = ' name="' + escapeXml(l.name) + '"'; + if (l.level) attrsStr += ' level="' + escapeXml(l.level) + '"'; + if (l.additivity === false) attrsStr += ' additivity="false"'; + blocks.push(' '); + }); + + var rootChildren = state.appenders + .filter(function (a) { return a.attachToRoot; }) + .map(function (a) { return ' '; }); + var rootBlock = ' '; + if (rootChildren.length) { + rootBlock += '\n' + rootChildren.join('\n') + '\n '; + } else { + rootBlock += '\n '; + } + blocks.push(rootBlock); + + lines.push(blocks.join('\n\n')); + lines.push(''); + return lines.join('\n'); + } + + // --------------------------------------------------------------------- + // XML syntax highlighting (tokenizes; single-pass replaces, no rescans) + // --------------------------------------------------------------------- + + function escapeHtml(s) { + return s.replace(/&/g, '&').replace(//g, '>'); + } + + function highlightXml(xml) { + var out = ''; + var tokens = xml.match(/|<[^>]+>|[^<]+/g) || []; + tokens.forEach(function (t) { + if (t.lastIndexOf('