onlylogs 0.1.2 → 0.1.3
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 +4 -4
- data/app/views/onlylogs/logs/index.html.erb +1 -49
- data/bin/onlylogs_sidecar +112 -0
- data/lib/onlylogs/configuration.rb +0 -4
- data/lib/onlylogs/version.rb +1 -1
- metadata +4 -5
- data/config/puma_plugins/vector.rb +0 -94
- data/config/udp_logger.rb +0 -40
- data/config/vector.toml +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5d17b58f1bc49d7f70ea61867d8c8e78be1fc25f1ede62f0b477764c97b78752
|
|
4
|
+
data.tar.gz: 3de1a2520cfb58fc74eab4152a43e20dc0082ec5099272efee00761c15ce3e1e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7079c7f67be0048382b9029ef636a17c6de654c49ae1c17346cfbfad99e7015a1ffed9a1ef1c4313b45f6cad2bed839dc804f39b616f46bb55d4203c3cde8bc8
|
|
7
|
+
data.tar.gz: 4630f69bf7066024413e4251747cc0da907f8d7912eab6eae78fb1c4b9ac3fa4d2bb026e0cc5e9452e9e78c289bf120c82724207426f88e7886b6ea92077d337
|
|
@@ -1,49 +1 @@
|
|
|
1
|
-
|
|
2
|
-
<aside id="sidebar">
|
|
3
|
-
<div class="sidebar-menu">
|
|
4
|
-
<%= link_to onlylogs.root_path, class: "btn sidebar-menu__button" do %>
|
|
5
|
-
<%= image_tag "onlylogs/logo.png" %>
|
|
6
|
-
<% end %>
|
|
7
|
-
<div class="sidebar-menu__content">
|
|
8
|
-
<div class="sidebar-menu__items">
|
|
9
|
-
<div class="sidebar-menu__group">
|
|
10
|
-
<div class="sidebar-menu__group-label">Projects</div>
|
|
11
|
-
<nav class="sidebar-menu__items">
|
|
12
|
-
<%= link_to "All Projects", root_path, class: "btn sidebar-menu__button" %>
|
|
13
|
-
|
|
14
|
-
<nav class="sidebar-menu__sub">
|
|
15
|
-
<nav class="sidebar-menu__sub text-sm">
|
|
16
|
-
<div>
|
|
17
|
-
<% if Onlylogs::File.text_file?(drain_file) %>
|
|
18
|
-
<%= link_to drain_file.basename, project_path(@project, file_path: Onlylogs::SecureFilePath.encrypt(drain_file.to_s)) %>
|
|
19
|
-
<% else %>
|
|
20
|
-
<%= drain_file.basename %>
|
|
21
|
-
<% end %>
|
|
22
|
-
<small>(<%= number_to_human_size(File.size(drain_file)) %>)</small>
|
|
23
|
-
<%= link_to project_drain_file_path(@project, Onlylogs::SecureFilePath.encrypt(drain_file.to_s)),
|
|
24
|
-
title: t('drain_files.show.title'),
|
|
25
|
-
class: 'download-link' do %>
|
|
26
|
-
<i class="fa-regular fa-download" aria-label="<%= t('drain_files.show.title') %>"></i>
|
|
27
|
-
<% end %>
|
|
28
|
-
</div>
|
|
29
|
-
</nav>
|
|
30
|
-
</nav>
|
|
31
|
-
</nav>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
</aside>
|
|
38
|
-
<main id="main">
|
|
39
|
-
<header class="flex items-center gap show@md">
|
|
40
|
-
<button type="button" class="btn btn--borderless p-1" data-action="toggle-class#toggle">
|
|
41
|
-
<i class="fa-regular fa-sidebar"></i>
|
|
42
|
-
<span class="sr-only">Toggle Sidebar</span>
|
|
43
|
-
</button>
|
|
44
|
-
<div class="separator-vertical mi-1" style="--sep-size: 1rem"></div>
|
|
45
|
-
</header>
|
|
46
|
-
|
|
47
|
-
<%= render partial: "onlylogs/shared/log_container", locals: { log_file_path: @log_file_path, tail: @max_lines, filter: @filter, autoscroll: @autoscroll } %>
|
|
48
|
-
</main>
|
|
49
|
-
</div>
|
|
1
|
+
<%= render partial: "onlylogs/shared/log_container", locals: { log_file_path: @log_file_path, tail: @max_lines, filter: @filter, autoscroll: @autoscroll } %>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# this is a simple sidecar process that takes log lines sent into the socket and sends them to the drain url in batches.
|
|
5
|
+
# You can also attach it to puma using `plugin :onlylogs_sidecar` in your config/puma.rb file so you don't need to run it manually.
|
|
6
|
+
# The following parameters are availble:
|
|
7
|
+
# - ONLYLOGS_DRAIN_URL: the url to the drain. this is mandatory and must be set.
|
|
8
|
+
# - ONLYLOGS_SIDECAR_SOCKET: the path to the socket file. this is optional and will default to tmp/sockets/onlylogs-sidecar.sock in the current directory.
|
|
9
|
+
# - ONLYLOGS_BATCH_SIZE: the number of lines to batch before sending. this is optional and will default to 100.
|
|
10
|
+
# - ONLYLOGS_FLUSH_INTERVAL: the interval in seconds to flush the batch. this is optional and will default to 0.5 seconds.
|
|
11
|
+
|
|
12
|
+
require "socket"
|
|
13
|
+
require "net/http"
|
|
14
|
+
require "uri"
|
|
15
|
+
require "thread"
|
|
16
|
+
require "fileutils"
|
|
17
|
+
|
|
18
|
+
drain_url = ENV["ONLYLOGS_DRAIN_URL"]
|
|
19
|
+
socket_path = ENV.fetch("ONLYLOGS_SIDECAR_SOCKET", File.expand_path("tmp/sockets/onlylogs-sidecar.sock", Dir.pwd))
|
|
20
|
+
batch_size = ENV.fetch("ONLYLOGS_BATCH_SIZE", "100").to_i
|
|
21
|
+
flush_interval = ENV.fetch("ONLYLOGS_FLUSH_INTERVAL", "0.5").to_f
|
|
22
|
+
|
|
23
|
+
if drain_url.to_s.strip.empty?
|
|
24
|
+
error "[OnlylogsSidecar] ERROR: ONLYLOGS_DRAIN_URL is not set. Exiting."
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
uri = URI.parse(drain_url)
|
|
29
|
+
FileUtils.mkdir_p(File.dirname(socket_path))
|
|
30
|
+
FileUtils.rm_f(socket_path)
|
|
31
|
+
|
|
32
|
+
running = true
|
|
33
|
+
queue = Queue.new
|
|
34
|
+
|
|
35
|
+
trap("INT") { running = false }
|
|
36
|
+
trap("TERM") { running = false }
|
|
37
|
+
|
|
38
|
+
server = UNIXServer.new(socket_path)
|
|
39
|
+
|
|
40
|
+
accept_thread = Thread.new do
|
|
41
|
+
while running
|
|
42
|
+
begin
|
|
43
|
+
client = server.accept_nonblock
|
|
44
|
+
rescue IO::WaitReadable
|
|
45
|
+
IO.select([server])
|
|
46
|
+
retry
|
|
47
|
+
rescue IOError, Errno::EBADF
|
|
48
|
+
break
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Thread.new(client) do |conn|
|
|
52
|
+
begin
|
|
53
|
+
conn.each_line do |line|
|
|
54
|
+
cleaned = line.to_s.strip
|
|
55
|
+
queue << cleaned unless cleaned.empty?
|
|
56
|
+
end
|
|
57
|
+
rescue => e
|
|
58
|
+
warn "[OnlylogsSidecar] client error: #{e.class}: #{e.message}"
|
|
59
|
+
ensure
|
|
60
|
+
conn.close rescue nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def send_batch(uri, lines)
|
|
67
|
+
return if lines.empty?
|
|
68
|
+
|
|
69
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
70
|
+
http.use_ssl = (uri.scheme == "https")
|
|
71
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
72
|
+
request.body = lines.join("\n")
|
|
73
|
+
puts "[OnlylogsSidecar] Sending #{request.body}"
|
|
74
|
+
request.content_type = "text/plain"
|
|
75
|
+
http.read_timeout = 5
|
|
76
|
+
http.open_timeout = 2
|
|
77
|
+
http.start
|
|
78
|
+
http.request(request)
|
|
79
|
+
rescue => e
|
|
80
|
+
warn "[OnlylogsSidecar] HTTP error: #{e.class}: #{e.message}"
|
|
81
|
+
ensure
|
|
82
|
+
http&.finish rescue nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
batch = []
|
|
86
|
+
last_flush = Time.now
|
|
87
|
+
|
|
88
|
+
while running || !queue.empty?
|
|
89
|
+
begin
|
|
90
|
+
line = queue.pop(true)
|
|
91
|
+
batch << line if line
|
|
92
|
+
rescue ThreadError
|
|
93
|
+
# queue empty
|
|
94
|
+
sleep 0.01
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
next if batch.empty?
|
|
98
|
+
|
|
99
|
+
if batch.size >= batch_size || (Time.now - last_flush) >= flush_interval
|
|
100
|
+
send_batch(uri, batch)
|
|
101
|
+
batch.clear
|
|
102
|
+
last_flush = Time.now
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Final flush
|
|
107
|
+
send_batch(uri, batch) unless batch.empty?
|
|
108
|
+
|
|
109
|
+
running = false
|
|
110
|
+
accept_thread.kill
|
|
111
|
+
server.close rescue nil
|
|
112
|
+
FileUtils.rm_f(socket_path)
|
data/lib/onlylogs/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: onlylogs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessandro Rodi
|
|
@@ -27,7 +27,8 @@ description: This gem includes all the tools needed to view and stream you log f
|
|
|
27
27
|
directly on a web interface.
|
|
28
28
|
email:
|
|
29
29
|
- alessandro.rodi@renuo.ch
|
|
30
|
-
executables:
|
|
30
|
+
executables:
|
|
31
|
+
- onlylogs_sidecar
|
|
31
32
|
extensions: []
|
|
32
33
|
extra_rdoc_files: []
|
|
33
34
|
files:
|
|
@@ -67,11 +68,9 @@ files:
|
|
|
67
68
|
- app/views/onlylogs/logs/index.html.erb
|
|
68
69
|
- app/views/onlylogs/shared/_log_container.html.erb
|
|
69
70
|
- app/views/onlylogs/shared/_log_container_styles.html.erb
|
|
71
|
+
- bin/onlylogs_sidecar
|
|
70
72
|
- config/importmap.rb
|
|
71
|
-
- config/puma_plugins/vector.rb
|
|
72
73
|
- config/routes.rb
|
|
73
|
-
- config/udp_logger.rb
|
|
74
|
-
- config/vector.toml
|
|
75
74
|
- db/migrate/20250902112548_create_books.rb
|
|
76
75
|
- lib/onlylogs.rb
|
|
77
76
|
- lib/onlylogs/configuration.rb
|
|
@@ -1,94 +0,0 @@
|
|
|
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/udp_logger.rb
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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
|
-
|