onlylogs 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +311 -0
- data/Rakefile +8 -0
- data/app/assets/config/onlylogs_manifest.js +2 -0
- data/app/assets/images/onlylogs/favicon/apple-touch-icon.png +0 -0
- data/app/assets/images/onlylogs/favicon/favicon-96x96.png +0 -0
- data/app/assets/images/onlylogs/favicon/favicon.ico +0 -0
- data/app/assets/images/onlylogs/favicon/favicon.svg +3 -0
- data/app/assets/images/onlylogs/favicon/site.webmanifest.erb +21 -0
- data/app/assets/images/onlylogs/favicon/web-app-manifest-192x192.png +0 -0
- data/app/assets/images/onlylogs/favicon/web-app-manifest-512x512.png +0 -0
- data/app/assets/images/onlylogs/logo.png +0 -0
- data/app/channels/onlylogs/application_cable/channel.rb +11 -0
- data/app/channels/onlylogs/logs_channel.rb +181 -0
- data/app/controllers/onlylogs/application_controller.rb +22 -0
- data/app/controllers/onlylogs/logs_controller.rb +23 -0
- data/app/helpers/onlylogs/application_helper.rb +4 -0
- data/app/javascript/onlylogs/application.js +1 -0
- data/app/javascript/onlylogs/controllers/application.js +9 -0
- data/app/javascript/onlylogs/controllers/index.js +11 -0
- data/app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js +46 -0
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +432 -0
- data/app/javascript/onlylogs/controllers/text_selection_controller.js +90 -0
- data/app/jobs/onlylogs/application_job.rb +4 -0
- data/app/models/onlylogs/ansi_color_parser.rb +78 -0
- data/app/models/onlylogs/application_record.rb +5 -0
- data/app/models/onlylogs/batch_sender.rb +61 -0
- data/app/models/onlylogs/file.rb +151 -0
- data/app/models/onlylogs/file_path_parser.rb +118 -0
- data/app/models/onlylogs/grep.rb +54 -0
- data/app/models/onlylogs/log_line.rb +24 -0
- data/app/models/onlylogs/secure_file_path.rb +31 -0
- data/app/views/home/show.html.erb +10 -0
- data/app/views/layouts/onlylogs/application.html.erb +27 -0
- data/app/views/onlylogs/logs/index.html.erb +49 -0
- data/app/views/onlylogs/shared/_log_container.html.erb +106 -0
- data/app/views/onlylogs/shared/_log_container_styles.html.erb +228 -0
- data/config/importmap.rb +6 -0
- data/config/puma_plugins/vector.rb +94 -0
- data/config/routes.rb +4 -0
- data/config/udp_logger.rb +40 -0
- data/config/vector.toml +32 -0
- data/db/migrate/20250902112548_create_books.rb +9 -0
- data/lib/onlylogs/configuration.rb +133 -0
- data/lib/onlylogs/engine.rb +39 -0
- data/lib/onlylogs/formatter.rb +14 -0
- data/lib/onlylogs/log_silencer_middleware.rb +26 -0
- data/lib/onlylogs/logger.rb +10 -0
- data/lib/onlylogs/socket_logger.rb +71 -0
- data/lib/onlylogs/version.rb +3 -0
- data/lib/onlylogs.rb +17 -0
- data/lib/puma/plugin/onlylogs_sidecar.rb +113 -0
- data/lib/tasks/onlylogs_tasks.rake +4 -0
- metadata +110 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<%# locals: (log_file_path:, tail: 100, filter: "", autoscroll: true) %>
|
|
2
|
+
<script src="https://cdn.jsdelivr.net/npm/clusterize.js@0.18.1/clusterize.min.js"></script>
|
|
3
|
+
<%= render "onlylogs/shared/log_container_styles" %>
|
|
4
|
+
|
|
5
|
+
<%
|
|
6
|
+
mode = filter.blank? ? "live" : "search"
|
|
7
|
+
cursor_position = mode == "search" ? 0 : [File.size(log_file_path) - (tail * 100), 0].max
|
|
8
|
+
|
|
9
|
+
raise SecurityError, "File path not allowed" unless Onlylogs.allowed_file_path?(log_file_path)
|
|
10
|
+
|
|
11
|
+
encrypted_log_file_path = Onlylogs::SecureFilePath.encrypt(log_file_path)
|
|
12
|
+
%>
|
|
13
|
+
|
|
14
|
+
<div data-controller="log-streamer text-selection keyboard-shortcuts"
|
|
15
|
+
data-log-streamer-file-path-value="<%= encrypted_log_file_path %>"
|
|
16
|
+
data-log-streamer-cursor-position-value="<%= cursor_position %>"
|
|
17
|
+
data-log-streamer-filter-value="<%= filter %>"
|
|
18
|
+
data-log-streamer-auto-scroll-value="<%= autoscroll %>"
|
|
19
|
+
data-log-streamer-mode-value="<%= mode %>"
|
|
20
|
+
class="onlylogs-log-container" >
|
|
21
|
+
<div data-log-streamer-target="logLines" data-text-selection-target="logLines" id="scrollArea" class="onlylogs-log-lines clusterize-scroll">
|
|
22
|
+
<div id="contentArea" class="clusterize-content">
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<button type="button"
|
|
27
|
+
data-text-selection-target="button"
|
|
28
|
+
class="onlylogs-context-menu"
|
|
29
|
+
data-action="click->text-selection#searchSelectedText"
|
|
30
|
+
title="Search selected text"
|
|
31
|
+
style="display: none;">
|
|
32
|
+
🔍 Search
|
|
33
|
+
</button>
|
|
34
|
+
|
|
35
|
+
<div class="onlylogs-log-toolbar">
|
|
36
|
+
<div>
|
|
37
|
+
<label style="margin-bottom: 0;">
|
|
38
|
+
<input id="liveMode" type="checkbox" <%= mode == "live" ? "checked" : "" %> name="liveMode" data-log-streamer-target="liveMode" data-keyboard-shortcuts-target="liveMode" data-action="change->log-streamer#toggleLiveMode">
|
|
39
|
+
<u>L</u>ive Mode
|
|
40
|
+
</label>
|
|
41
|
+
</div>
|
|
42
|
+
<div>
|
|
43
|
+
<label style="margin-bottom: 0;">
|
|
44
|
+
<input id="autoscroll" type="checkbox" <%= autoscroll ? "checked" : "" %> name="autoscroll" data-keyboard-shortcuts-target="autoscroll" data-action="change->log-streamer#toggleAutoScroll">
|
|
45
|
+
<u>A</u>utoscroll
|
|
46
|
+
</label>
|
|
47
|
+
</div>
|
|
48
|
+
<div>
|
|
49
|
+
<label style="margin-bottom: 0;">
|
|
50
|
+
<input id="regexpMode" type="checkbox" name="regexpMode" data-log-streamer-target="regexpMode" data-text-selection-target="regexpMode" data-action="change->log-streamer#toggleRegexpMode">
|
|
51
|
+
<u>R</u>egexp Mode
|
|
52
|
+
</label>
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<label style="margin-bottom: 0;">
|
|
56
|
+
Filter:
|
|
57
|
+
<div style="display: inline-flex; align-items: center; position: relative;">
|
|
58
|
+
<input type="text"
|
|
59
|
+
name="filter"
|
|
60
|
+
value="<%= filter %>"
|
|
61
|
+
placeholder="Enter filter text..."
|
|
62
|
+
data-log-streamer-target="filterInput"
|
|
63
|
+
data-text-selection-target="filterInput"
|
|
64
|
+
data-action="input->log-streamer#applyFilter"
|
|
65
|
+
style="padding-right: 1.5rem;">
|
|
66
|
+
<button type="button"
|
|
67
|
+
data-action="click->log-streamer#clearFilter"
|
|
68
|
+
class="clear-filter-button"
|
|
69
|
+
title="Clear filter">
|
|
70
|
+
×
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
</label>
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<button type="button"
|
|
77
|
+
data-log-streamer-target="stopButton"
|
|
78
|
+
data-action="click->log-streamer#stopSearch"
|
|
79
|
+
class="stop-search-button"
|
|
80
|
+
title="Stop current search"
|
|
81
|
+
style="display: none;">
|
|
82
|
+
⏹️ Stop
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
<div>
|
|
86
|
+
<span data-log-streamer-target="lineRange" style="color: #666;">No lines</span>
|
|
87
|
+
</div>
|
|
88
|
+
<div data-log-streamer-target="message"></div>
|
|
89
|
+
<% unless Onlylogs.ripgrep_enabled? %>
|
|
90
|
+
<span class="grep-warning" title="Search is slow. Install ripgrep for better performance">⚠️</span>
|
|
91
|
+
<% end %>
|
|
92
|
+
<div class="file-size" title="File size: <%= number_to_human_size(File.size(log_file_path)) %>">
|
|
93
|
+
<%= number_to_human_size(File.size(log_file_path)) %>
|
|
94
|
+
</div>
|
|
95
|
+
<div style="width:67px">
|
|
96
|
+
<button type="button"
|
|
97
|
+
data-log-streamer-target="clearButton"
|
|
98
|
+
data-action="click->log-streamer#clearLogs"
|
|
99
|
+
class="clear-logs-button"
|
|
100
|
+
title="Clear all log lines">
|
|
101
|
+
🗑️ Clear
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
<div data-log-streamer-target="websocketStatus" class="websocket-status websocket-status--disconnected" title="WebSocket Disconnected">🔴</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.onlylogs-log-container {
|
|
3
|
+
background-color: var(--onlylogs-bg-color, none);
|
|
4
|
+
width: var(--onlylogs-width, 100%);
|
|
5
|
+
height: var(--onlylogs-height, 100%); /* Take full height of parent container */
|
|
6
|
+
min-height: var(--onlylogs-min-height, 400px);
|
|
7
|
+
display: grid;
|
|
8
|
+
grid-template-rows: auto var(--onlylogs-toolbar-height, 42px);
|
|
9
|
+
|
|
10
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
11
|
+
font-size: var(--onlylogs-font-size, 0.8rem);
|
|
12
|
+
margin: 0;
|
|
13
|
+
border: none;
|
|
14
|
+
border-radius: 0;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
position: relative;
|
|
17
|
+
|
|
18
|
+
.onlylogs-log-lines {
|
|
19
|
+
overflow-y: auto;
|
|
20
|
+
overflow-x: auto;
|
|
21
|
+
|
|
22
|
+
pre {
|
|
23
|
+
overflow: visible;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.clusterize-content {
|
|
28
|
+
outline: 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.line-number {
|
|
32
|
+
color: #aaa;
|
|
33
|
+
user-select: none;
|
|
34
|
+
margin-right: 0.5em;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.color-success {
|
|
38
|
+
color: green;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.color-error {
|
|
42
|
+
color: red;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.fw-bold {
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.log-black {
|
|
50
|
+
color: lch(18% 0 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.log-red {
|
|
54
|
+
color: lch(55% 60 29);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.log-green {
|
|
58
|
+
color: lch(70% 45 135);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.log-yellow {
|
|
62
|
+
color: lch(85% 30 100);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.log-blue {
|
|
66
|
+
color: lch(65% 45 260);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.log-magenta {
|
|
70
|
+
color: lch(65% 40 320);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.log-cyan {
|
|
74
|
+
color: lch(75% 30 200);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.log-white {
|
|
78
|
+
color: lch(95% 2 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pre {
|
|
82
|
+
margin: 0 !important;
|
|
83
|
+
padding: 0.2rem;
|
|
84
|
+
word-break: break-word; /* allow breaking long tokens like UUIDs/SQL */
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
a {
|
|
88
|
+
color: inherit;
|
|
89
|
+
text-decoration: underline;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.onlylogs-context-menu {
|
|
93
|
+
position: absolute;
|
|
94
|
+
z-index: 1000;
|
|
95
|
+
background: #e3f2fd;
|
|
96
|
+
border: 1px solid #90caf9;
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
99
|
+
padding: 6px 12px;
|
|
100
|
+
font-size: 0.9em;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
transition: background-color 0.2s;
|
|
103
|
+
color: #1976d2;
|
|
104
|
+
font-weight: 500;
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: 4px;
|
|
108
|
+
|
|
109
|
+
&:active {
|
|
110
|
+
background-color: #90caf9;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
&:hover {
|
|
114
|
+
background-color: #bbdefb;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@keyframes spin {
|
|
119
|
+
from { transform: rotate(0deg); }
|
|
120
|
+
to { transform: rotate(360deg); }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.onlylogs-log-toolbar {
|
|
124
|
+
flex-shrink: 0;
|
|
125
|
+
padding: 1rem 0.5rem;
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 1.5rem;
|
|
129
|
+
|
|
130
|
+
.live-mode-sticky {
|
|
131
|
+
opacity: 0.7;
|
|
132
|
+
cursor: not-allowed;
|
|
133
|
+
|
|
134
|
+
input[type="checkbox"] {
|
|
135
|
+
cursor: not-allowed;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.clear-filter-button {
|
|
140
|
+
position: absolute;
|
|
141
|
+
right: 0.25rem;
|
|
142
|
+
background: none;
|
|
143
|
+
border: none;
|
|
144
|
+
color: #666;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
font-size: 0.8rem;
|
|
147
|
+
padding: 0;
|
|
148
|
+
width: 1rem;
|
|
149
|
+
height: 1rem;
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
justify-content: center;
|
|
153
|
+
padding: 0.25rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.onlylogs-spin-animation {
|
|
157
|
+
display: inline-block;
|
|
158
|
+
animation: spin 1s linear infinite;
|
|
159
|
+
margin-right: 0.5em;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.grep-warning {
|
|
163
|
+
color: #f59e0b;
|
|
164
|
+
font-size: 1.2em;
|
|
165
|
+
cursor: help;
|
|
166
|
+
margin-left: auto;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.file-size {
|
|
170
|
+
color: #666;
|
|
171
|
+
cursor: help;
|
|
172
|
+
margin-left: auto;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.websocket-status {
|
|
176
|
+
cursor: help;
|
|
177
|
+
border-radius: 50%;
|
|
178
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
179
|
+
|
|
180
|
+
&--connected {
|
|
181
|
+
background-color: rgba(34, 197, 94, 0.2);
|
|
182
|
+
animation: pulse 2s infinite;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
&--disconnected {
|
|
186
|
+
background-color: rgba(239, 68, 68, 0.2);
|
|
187
|
+
animation: pulse 2s infinite;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
&--rejected {
|
|
191
|
+
background-color: rgba(245, 158, 11, 0.2);
|
|
192
|
+
animation: pulse 2s infinite;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@keyframes pulse {
|
|
197
|
+
0%, 100% { opacity: 1; }
|
|
198
|
+
50% { opacity: 0.6; }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.stop-search-button {
|
|
202
|
+
background-color: #dc2626;
|
|
203
|
+
color: white;
|
|
204
|
+
border: none;
|
|
205
|
+
padding: 0.25rem 0.5rem;
|
|
206
|
+
border-radius: 0.25rem;
|
|
207
|
+
transition: background-color 0.2s;
|
|
208
|
+
|
|
209
|
+
&:hover {
|
|
210
|
+
background-color: #b91c1c;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
&:active {
|
|
214
|
+
background-color: #991b1b;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.error-message {
|
|
219
|
+
color: #dc2626;
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
padding: 0.5rem;
|
|
222
|
+
background-color: rgba(220, 38, 38, 0.1);
|
|
223
|
+
border-radius: 0.25rem;
|
|
224
|
+
display: inline-block;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
</style>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pin "application", to: "onlylogs/application.js", preload: true
|
|
2
|
+
pin "@rails/actioncable", to: "actioncable.esm.js"
|
|
3
|
+
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
|
|
4
|
+
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
|
|
5
|
+
pin_all_from Onlylogs::Engine.root.join("app/javascript/onlylogs/controllers"), under: "controllers", to: "onlylogs/controllers"
|
|
6
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "puma/plugin"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "shellwords"
|
|
7
|
+
|
|
8
|
+
Puma::Plugin.create do
|
|
9
|
+
def start(launcher)
|
|
10
|
+
@launcher = launcher
|
|
11
|
+
@events = launcher.events
|
|
12
|
+
@options = launcher.config.options
|
|
13
|
+
@vector_pid = nil
|
|
14
|
+
|
|
15
|
+
setup_paths
|
|
16
|
+
start_vector
|
|
17
|
+
register_hooks
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def setup_paths
|
|
23
|
+
@app_root = @options[:directory] || Dir.pwd
|
|
24
|
+
@vector_bin = env_or_option("ONLYLOGS_VECTOR_BIN", :onlylogs_vector_bin, "vector")
|
|
25
|
+
@vector_config = env_or_option(
|
|
26
|
+
"ONLYLOGS_VECTOR_CONFIG",
|
|
27
|
+
:onlylogs_vector_config,
|
|
28
|
+
File.expand_path("../vector.toml", __dir__)
|
|
29
|
+
)
|
|
30
|
+
@vector_args = env_or_option("ONLYLOGS_VECTOR_ARGS", :onlylogs_vector_args, "")
|
|
31
|
+
@dsn = env_or_option("ONLYLOGS_DSN", :onlylogs_dsn, "https://onlylogs.io/drain/testmac")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def register_hooks
|
|
35
|
+
events = @launcher.events
|
|
36
|
+
events.register(:on_restart) { restart_vector }
|
|
37
|
+
at_exit { stop_vector }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def env_or_option(env_key, option_key, default)
|
|
41
|
+
ENV.fetch(env_key, @options.fetch(option_key, default))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def start_vector
|
|
45
|
+
stop_vector if @vector_pid
|
|
46
|
+
|
|
47
|
+
unless File.exist?(@vector_config)
|
|
48
|
+
warn "Vector config not found at #{@vector_config}; skipping start"
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
args = [ @vector_bin, "--config", @vector_config ]
|
|
53
|
+
args += Shellwords.split(@vector_args.to_s) unless @vector_args.to_s.empty?
|
|
54
|
+
|
|
55
|
+
info "Starting Vector sidecar (config: #{@vector_config}, dsn: #{@dsn})"
|
|
56
|
+
env = { "ONLYLOGS_DSN" => @dsn }
|
|
57
|
+
@vector_pid = Process.spawn(env, *args, chdir: @app_root, pgroup: true)
|
|
58
|
+
rescue Errno::ENOENT => e
|
|
59
|
+
error "Unable to start Vector sidecar: #{e.message}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def restart_vector
|
|
63
|
+
info "Restarting Vector sidecar"
|
|
64
|
+
start_vector
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stop_vector
|
|
68
|
+
return unless @vector_pid
|
|
69
|
+
|
|
70
|
+
info "Stopping Vector sidecar"
|
|
71
|
+
pgid = Process.getpgid(@vector_pid)
|
|
72
|
+
Process.kill("TERM", -pgid)
|
|
73
|
+
Timeout.timeout(5) { Process.wait(@vector_pid) }
|
|
74
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
75
|
+
# already stopped
|
|
76
|
+
rescue Timeout::Error
|
|
77
|
+
warn "Vector sidecar did not stop in time, killing"
|
|
78
|
+
Process.kill("KILL", -pgid) rescue nil
|
|
79
|
+
ensure
|
|
80
|
+
@vector_pid = nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def info(message)
|
|
84
|
+
@events.log("[VectorSidecar] #{message}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def warn(message)
|
|
88
|
+
@events.log("[VectorSidecar][WARN] #{message}")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def error(message)
|
|
92
|
+
@events.error("[VectorSidecar][ERROR] #{message}")
|
|
93
|
+
end
|
|
94
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# udp_logger.rb
|
|
2
|
+
require "logger"
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
class UdpLogger < Logger
|
|
6
|
+
def initialize(host: "127.0.0.1", port: 6000, local_fallback: $stdout)
|
|
7
|
+
# Use a normal Logger underneath so we still see logs locally
|
|
8
|
+
super(local_fallback)
|
|
9
|
+
|
|
10
|
+
@udp_host = host
|
|
11
|
+
@udp_port = port
|
|
12
|
+
@socket = UDPSocket.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Override Logger#add, which all the level methods delegate to
|
|
16
|
+
def add(severity, message = nil, progname = nil, &block)
|
|
17
|
+
# Same semantics as Logger:
|
|
18
|
+
if message.nil?
|
|
19
|
+
if block_given?
|
|
20
|
+
message = block.call
|
|
21
|
+
else
|
|
22
|
+
message = progname
|
|
23
|
+
progname = nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Send plain text over UDP to Vector
|
|
28
|
+
begin
|
|
29
|
+
payload = message.to_s
|
|
30
|
+
@socket.send(payload, 0, @udp_host, @udp_port)
|
|
31
|
+
rescue => e
|
|
32
|
+
# Swallow UDP errors so logging never crashes the app
|
|
33
|
+
warn "UDP logger error: #{e.class}: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Also log locally (stdout / file) via normal Logger behavior
|
|
37
|
+
super(severity, message, progname, &block)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
data/config/vector.toml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Where Vector keeps internal state (buffers, etc.)
|
|
2
|
+
data_dir = "/usr/local/var/lib/vector"
|
|
3
|
+
|
|
4
|
+
# --- 1) SOURCE: UDP socket listening on localhost:6000 ---
|
|
5
|
+
|
|
6
|
+
[sources.udp_logs]
|
|
7
|
+
type = "socket"
|
|
8
|
+
mode = "udp" # UDP mode
|
|
9
|
+
address = "127.0.0.1:6000"
|
|
10
|
+
|
|
11
|
+
# --- 2) SINK: console (for debugging, optional but very handy) ---
|
|
12
|
+
|
|
13
|
+
[sinks.console]
|
|
14
|
+
type = "console"
|
|
15
|
+
inputs = ["udp_logs"]
|
|
16
|
+
encoding.codec = "json"
|
|
17
|
+
target = "stdout"
|
|
18
|
+
|
|
19
|
+
# --- 3) SINK: Onlylogs HTTP drain ---
|
|
20
|
+
|
|
21
|
+
[sinks.onlylogs]
|
|
22
|
+
type = "http"
|
|
23
|
+
inputs = ["udp_logs"] # consume events from udp_logs
|
|
24
|
+
method = "post"
|
|
25
|
+
uri = "${ONLYLOGS_DSN}"
|
|
26
|
+
|
|
27
|
+
encoding.codec = "text"
|
|
28
|
+
|
|
29
|
+
#[sinks.onlylogs.request]
|
|
30
|
+
# Adjust headers if Onlylogs expects something specific
|
|
31
|
+
#headers.Content-Type = "application/json"
|
|
32
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Onlylogs
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :allowed_files, :default_log_file_path, :basic_auth_user, :basic_auth_password,
|
|
6
|
+
:parent_controller, :disable_basic_authentication, :ripgrep_enabled, :editor,
|
|
7
|
+
:max_line_matches
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@allowed_files = default_allowed_files
|
|
11
|
+
@default_log_file_path = default_log_file_path_value
|
|
12
|
+
@basic_auth_user = default_basic_auth_user
|
|
13
|
+
@basic_auth_password = default_basic_auth_password
|
|
14
|
+
@parent_controller = nil
|
|
15
|
+
@disable_basic_authentication = false
|
|
16
|
+
@ripgrep_enabled = default_ripgrep_enabled
|
|
17
|
+
@editor = default_editor
|
|
18
|
+
@max_line_matches = 100000
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configure
|
|
22
|
+
yield self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def default_editor
|
|
26
|
+
if (credentials_editor = Rails.application.credentials.dig(:onlylogs, :editor))
|
|
27
|
+
return credentials_editor
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 2. Check environment variables (ONLYLOGS_EDITOR > RAILS_EDITOR > EDITOR)
|
|
31
|
+
if ENV["ONLYLOGS_EDITOR"]
|
|
32
|
+
return ENV["ONLYLOGS_EDITOR"].to_sym
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if ENV["RAILS_EDITOR"]
|
|
36
|
+
return ENV["RAILS_EDITOR"].to_sym
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if ENV["EDITOR"]
|
|
40
|
+
return ENV["EDITOR"].to_sym
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# 3. Default fallback
|
|
44
|
+
:vscode
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def default_allowed_files
|
|
48
|
+
# Default to environment-specific log files (without rotation suffixes)
|
|
49
|
+
[
|
|
50
|
+
Rails.root.join("log/#{Rails.env}.log")
|
|
51
|
+
]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def default_log_file_path_value
|
|
55
|
+
Rails.root.join("log/#{Rails.env}.log").to_s
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def default_basic_auth_user
|
|
59
|
+
ENV["ONLYLOGS_BASIC_AUTH_USER"] || Rails.application.credentials.dig(:onlylogs, :basic_auth_user)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def default_basic_auth_password
|
|
63
|
+
ENV["ONLYLOGS_BASIC_AUTH_PASSWORD"] || Rails.application.credentials.dig(:onlylogs, :basic_auth_password)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def default_ripgrep_enabled
|
|
67
|
+
system("which rg > /dev/null 2>&1")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.configuration
|
|
72
|
+
@configuration ||= Configuration.new
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.configure
|
|
76
|
+
yield configuration
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.allowed_file_path?(file_path)
|
|
80
|
+
path = ::File.expand_path(file_path.to_s)
|
|
81
|
+
|
|
82
|
+
configuration.allowed_files.any? do |pattern|
|
|
83
|
+
pat = ::File.expand_path(pattern.to_s)
|
|
84
|
+
::File.fnmatch?(pat, path, ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.allowed_files
|
|
89
|
+
configuration.allowed_files
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.default_log_file_path
|
|
93
|
+
configuration.default_log_file_path
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.basic_auth_user
|
|
97
|
+
configuration.basic_auth_user
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.basic_auth_password
|
|
101
|
+
configuration.basic_auth_password
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.parent_controller
|
|
105
|
+
configuration.parent_controller
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.disable_basic_authentication?
|
|
109
|
+
configuration.disable_basic_authentication
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.basic_auth_configured?
|
|
113
|
+
basic_auth_user.present? && basic_auth_password.present?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.ripgrep_enabled?
|
|
117
|
+
configuration.ripgrep_enabled
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.editor
|
|
121
|
+
configuration.default_editor
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.editor=(editor_symbol)
|
|
125
|
+
configuration.editor = editor_symbol
|
|
126
|
+
# Clear the cached editor instance when editor changes
|
|
127
|
+
Onlylogs::FilePathParser.clear_editor_cache
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.max_line_matches
|
|
131
|
+
configuration.max_line_matches
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "importmap-rails"
|
|
2
|
+
|
|
3
|
+
module Onlylogs
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Onlylogs
|
|
6
|
+
|
|
7
|
+
initializer "onlylogs.assets" do |app|
|
|
8
|
+
%w[images stylesheets builds fonts].each do |subdir|
|
|
9
|
+
path = root.join("app/assets", subdir)
|
|
10
|
+
app.config.assets.paths << path if path.exist?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
14
|
+
app.config.assets.precompile += %w[ onlylogs_manifest ]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
initializer "onlylogs.importmap", after: "importmap" do |app|
|
|
19
|
+
Onlylogs.importmap.draw(root.join("config/importmap.rb"))
|
|
20
|
+
if app.config.importmap.sweep_cache && app.config.reloading_enabled?
|
|
21
|
+
Onlylogs.importmap.cache_sweeper(watches: root.join("app/javascript"))
|
|
22
|
+
|
|
23
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
24
|
+
before_action { Onlylogs.importmap.cache_sweeper.execute_if_updated }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# initializer "onlylogs.add_log_silencer" do |app|
|
|
30
|
+
# silenced_routes = ['/onlylogs']
|
|
31
|
+
#
|
|
32
|
+
# app.middleware.insert_before(
|
|
33
|
+
# Rails::Rack::Logger,
|
|
34
|
+
# Onlylogs::LogSilencerMiddleware,
|
|
35
|
+
# paths_to_silence: silenced_routes
|
|
36
|
+
# )
|
|
37
|
+
# end
|
|
38
|
+
end
|
|
39
|
+
end
|