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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f2f05cff2b5f48634427295d3704774b1431c85f5bbba89a5adc9d3a6769c79
4
- data.tar.gz: 16fc0ca2ec651f9523e266ec883a796f2dc94bb46b952b5b6aaf0eb527fd8871
3
+ metadata.gz: 5d17b58f1bc49d7f70ea61867d8c8e78be1fc25f1ede62f0b477764c97b78752
4
+ data.tar.gz: 3de1a2520cfb58fc74eab4152a43e20dc0082ec5099272efee00761c15ce3e1e
5
5
  SHA512:
6
- metadata.gz: 7625fcd6b6b275a4008e88d7ec9a085b8277854f759472a7eea0acb98d2b8d81c14023ddb8eae313f74bf95d82b9ce8d134fdc0b52af019d84d560a2e62fb7e8
7
- data.tar.gz: 104a06ec0c73c0885a89cce09dd25597d4c013c2cac9ab9b88f32a77d785aba323885f6eb6cc7a28bfe87c0e0792777d1aae16e6188ffb7bcc589961b8e7a4f6
6
+ metadata.gz: 7079c7f67be0048382b9029ef636a17c6de654c49ae1c17346cfbfad99e7015a1ffed9a1ef1c4313b45f6cad2bed839dc804f39b616f46bb55d4203c3cde8bc8
7
+ data.tar.gz: 4630f69bf7066024413e4251747cc0da907f8d7912eab6eae78fb1c4b9ac3fa4d2bb026e0cc5e9452e9e78c289bf120c82724207426f88e7886b6ea92077d337
@@ -1,49 +1 @@
1
- <div class="sidebar-layout" data-controller="toggle-class" data-toggle-class-toggle-class="closed">
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)
@@ -85,10 +85,6 @@ module Onlylogs
85
85
  end
86
86
  end
87
87
 
88
- def self.allowed_files
89
- configuration.allowed_files
90
- end
91
-
92
88
  def self.default_log_file_path
93
89
  configuration.default_log_file_path
94
90
  end
@@ -1,3 +1,3 @@
1
1
  module Onlylogs
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
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.2
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
-