Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions config/puma_plugins/vector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true

require "puma/plugin"
require "rbconfig"
require "timeout"
require "shellwords"

Puma::Plugin.create do
def start(launcher)
@launcher = launcher
@events = launcher.events
@options = launcher.config.options
@vector_pid = nil

setup_paths
start_vector
register_hooks
end

private

def setup_paths
@app_root = @options[:directory] || Dir.pwd
@vector_bin = env_or_option("ONLYLOGS_VECTOR_BIN", :onlylogs_vector_bin, "vector")
@vector_config = env_or_option(
"ONLYLOGS_VECTOR_CONFIG",
:onlylogs_vector_config,
File.expand_path("../vector.toml", __dir__)
)
@vector_args = env_or_option("ONLYLOGS_VECTOR_ARGS", :onlylogs_vector_args, "")
@dsn = env_or_option("ONLYLOGS_DSN", :onlylogs_dsn, "https://onlylogs.io/drain/testmac")
end

def register_hooks
events = @launcher.events
events.register(:on_restart) { restart_vector }
at_exit { stop_vector }
end

def env_or_option(env_key, option_key, default)
ENV.fetch(env_key, @options.fetch(option_key, default))
end

def start_vector
stop_vector if @vector_pid

unless File.exist?(@vector_config)
warn "Vector config not found at #{@vector_config}; skipping start"
return
end

args = [ @vector_bin, "--config", @vector_config ]
args += Shellwords.split(@vector_args.to_s) unless @vector_args.to_s.empty?

info "Starting Vector sidecar (config: #{@vector_config}, dsn: #{@dsn})"
env = { "ONLYLOGS_DSN" => @dsn }
@vector_pid = Process.spawn(env, *args, chdir: @app_root, pgroup: true)
rescue Errno::ENOENT => e
error "Unable to start Vector sidecar: #{e.message}"
end

def restart_vector
info "Restarting Vector sidecar"
start_vector
end

def stop_vector
return unless @vector_pid

info "Stopping Vector sidecar"
pgid = Process.getpgid(@vector_pid)
Process.kill("TERM", -pgid)
Timeout.timeout(5) { Process.wait(@vector_pid) }
rescue Errno::ESRCH, Errno::ECHILD
# already stopped
rescue Timeout::Error
warn "Vector sidecar did not stop in time, killing"
Process.kill("KILL", -pgid) rescue nil
ensure
@vector_pid = nil
end

def info(message)
@events.log("[VectorSidecar] #{message}")
end

def warn(message)
@events.log("[VectorSidecar][WARN] #{message}")
end

def error(message)
@events.error("[VectorSidecar][ERROR] #{message}")
end
end
40 changes: 40 additions & 0 deletions config/udp_logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# udp_logger.rb
require "logger"
require "socket"

class UdpLogger < Logger
def initialize(host: "127.0.0.1", port: 6000, local_fallback: $stdout)
# Use a normal Logger underneath so we still see logs locally
super(local_fallback)

@udp_host = host
@udp_port = port
@socket = UDPSocket.new
end

# Override Logger#add, which all the level methods delegate to
def add(severity, message = nil, progname = nil, &block)
# Same semantics as Logger:
if message.nil?
if block_given?
message = block.call
else
message = progname
progname = nil
end
end

# Send plain text over UDP to Vector
begin
payload = message.to_s
@socket.send(payload, 0, @udp_host, @udp_port)
rescue => e
# Swallow UDP errors so logging never crashes the app
warn "UDP logger error: #{e.class}: #{e.message}"
end

# Also log locally (stdout / file) via normal Logger behavior
super(severity, message, progname, &block)
end
end

32 changes: 32 additions & 0 deletions config/vector.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Where Vector keeps internal state (buffers, etc.)
data_dir = "/usr/local/var/lib/vector"

# --- 1) SOURCE: UDP socket listening on localhost:6000 ---

[sources.udp_logs]
type = "socket"
mode = "udp" # UDP mode
address = "127.0.0.1:6000"

# --- 2) SINK: console (for debugging, optional but very handy) ---

[sinks.console]
type = "console"
inputs = ["udp_logs"]
encoding.codec = "json"
target = "stdout"

# --- 3) SINK: Onlylogs HTTP drain ---

[sinks.onlylogs]
type = "http"
inputs = ["udp_logs"] # consume events from udp_logs
method = "post"
uri = "${ONLYLOGS_DSN}"

encoding.codec = "text"

#[sinks.onlylogs.request]
# Adjust headers if Onlylogs expects something specific
#headers.Content-Type = "application/json"

Loading