Skip to content
Merged
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
63 changes: 63 additions & 0 deletions .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Lint, test, and build

on:
pull_request:
push:
branches:
- main
- master

permissions:
contents: read

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Format
run: make fmt

- name: Vet
run: make vet

test:
name: Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Run tests
run: make test

build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Build release binary
run: make build-release
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Local build artifacts
bin/

cmd/testing*
93 changes: 93 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
export GO111MODULE=on
BUILD_DIR ?= bin
BUILD_TAGS ?= osusergo,netgo,sqlite_omit_load_extension
LDFLAGS ?= -extldflags '-static' -s -w
PKG ?= ./cmd/coriolis-logger

.PHONY: all
all: build-release

.PHONY: fmt
fmt: ## Format the code.
go fmt ./...

.PHONY: vet
vet: ## Run static code analysis.
go vet ./...

COVER_OUTDIR ?= $(BUILD_DIR)
COVER_OUTFILE_RAW ?= $(COVER_OUTDIR)/coverage.raw
COVER_OUTFILE_HTML ?= $(COVER_OUTDIR)/coverage.html
# Filter executed tests by the "TEST_RE" regex
TEST_RE ?= .*
# Skip tests that match the "TEST_SKIP_RE" regex
TEST_SKIP_RE ?=
TEST_CMD = go test ./... \
-run "$(TEST_RE)" \
-coverpkg=coriolis-logger/... \
-coverprofile=$(COVER_OUTFILE_RAW)
ifneq ($(strip $(TEST_SKIP_RE)),)
TEST_CMD += -skip "$(TEST_SKIP_RE)"
endif

.PHONY: test-unit
test-unit: fmt vet ## Run coriolis-logger unit tests.
mkdir -p $(COVER_OUTDIR)
$(TEST_CMD)
go tool cover -html=$(COVER_OUTFILE_RAW) -o=$(COVER_OUTFILE_HTML)

.PHONY: test-unit-verbose
test-unit-verbose: fmt vet ## Run coriolis-logger unit tests in verbose mode.
mkdir -p $(COVER_OUTDIR)
$(TEST_CMD) -test.v
go tool cover -html=$(COVER_OUTFILE_RAW) -o=$(COVER_OUTFILE_HTML)

.PHONY: test
test: test-unit ## Run all coriolis-logger tests.

.PHONY: build-dev
build-dev: fmt vet ## Generate coriolis-logger dev build.
# Dev build, meant to build fast and run on the dev machine:
# * use the host architecture
# * avoid rebuilding unmodified components
mkdir -p $(BUILD_DIR)
go build \
-o $(BUILD_DIR)/coriolis-logger \
$(PKG)

.PHONY: build-dev-dbg
build-dev-dbg: fmt vet ## Generate coriolis-logger dev build, disabling compiler optimizations.
mkdir -p $(BUILD_DIR)
go build -gcflags="all=-N -l" \
-o $(BUILD_DIR)/coriolis-logger \
$(PKG)

.PHONY: build-release
build-release: fmt vet ## Generate coriolis-logger release build.
# Release build, meant to be deployed as the Coriolis logging service:
# * strip debug symbols
# * build for Linux x86_64
# * statically linked
# * rebuild everything
mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a \
-tags "$(BUILD_TAGS)" \
-ldflags="$(LDFLAGS)" \
-o $(BUILD_DIR)/coriolis-logger \
$(PKG)

CONFIG ?= testdata/config.toml

.PHONY: run
run: ## Run coriolis-logger.
$(BUILD_DIR)/coriolis-logger -config "$(CONFIG)"

# We'll reuse the "help" generator from operator-sdk (Apache-2).
.DEFAULT_GOAL := help
.PHONY: help
help: ## Show this help screen.
@echo 'Usage: make <OPTIONS> ... <TARGETS>'
@echo ''
@echo 'Available targets are:'
@echo ''
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ Build the binary:

```bash
cd coriolis-logger
make build-dev
```

This writes `bin/coriolis-logger`. Use `make build-release` for a Linux amd64 binary, and `make help` to list all targets.

Alternatively:

```bash
go install ./...
```

Expand Down Expand Up @@ -63,6 +71,12 @@ listener = "unixgram"
# address = "/tmp/coriolis-logger/syslog"
address = "/tmp/coriolis-logging.sock"

# Optional second listener. The syslog server can bind a unix
# datagram socket and a TCP or UDP endpoint at the same time.
# extra_listener and extra_address must both be set when used.
# extra_listener = "tcp"
# extra_address = "0.0.0.0:5144"

# Log format
# possible values:
# rfc3164
Expand Down
2 changes: 1 addition & 1 deletion cmd/coriolis-logger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import (
var log = loggo.GetLogger("coriolis.logger.cmd")

func main() {
stop := make(chan os.Signal)
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGTERM)
signal.Notify(stop, syscall.SIGINT)
log.SetLogLevel(loggo.DEBUG)
Expand Down
47 changes: 36 additions & 11 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,17 @@ func (a *APIServer) Validate() error {
}

type Syslog struct {
Listener ListenerType
Address string
Format string
LogToStdout bool `toml:"log_to_stdout"`
DataStore DatastoreType
InfluxDB *InfluxDB `toml:"influxdb"`
Listener ListenerType
Address string
// ExtraListener and ExtraAddress optionally start a second syslog
// endpoint on the same server. This allows a unix datagram socket
// to run alongside a TCP or UDP listener (or vice versa).
ExtraListener ListenerType `toml:"extra_listener"`
ExtraAddress string `toml:"extra_address"`
Format string
LogToStdout bool `toml:"log_to_stdout"`
DataStore DatastoreType
InfluxDB *InfluxDB `toml:"influxdb"`
}

func (s *Syslog) LogFormat() (format.Format, error) {
Expand Down Expand Up @@ -213,9 +218,29 @@ func (s *Syslog) Validate() error {
return fmt.Errorf("invalid datastore type %q", s.DataStore)
}

switch s.Listener {
if err := validateListener(s.Listener, s.Address); err != nil {
return err
}

if s.ExtraListener == "" && s.ExtraAddress == "" {
return nil
}
if s.ExtraListener == "" || s.ExtraAddress == "" {
return fmt.Errorf("extra_listener and extra_address must both be set")
}
if err := validateListener(s.ExtraListener, s.ExtraAddress); err != nil {
return errors.Wrap(err, "validating extra listener")
}
if s.Listener == s.ExtraListener && s.Address == s.ExtraAddress {
return fmt.Errorf("extra listener duplicates the primary listener")
}
return nil
}

func validateListener(listener ListenerType, address string) error {
switch listener {
case UnixDgramListener:
absPath, err := filepath.Abs(s.Address)
absPath, err := filepath.Abs(address)
if err != nil {
return errors.Wrap(err, "getting dirname")
}
Expand All @@ -224,15 +249,15 @@ func (s *Syslog) Validate() error {
return errors.Wrap(err, "fetching info about dirname")
}

if mode, err := os.Stat(s.Address); err == nil {
if mode, err := os.Stat(address); err == nil {
if mode.Mode()&os.ModeSocket == 0 {
return fmt.Errorf(
"cannot use %q as address. File already exists and is not socket", s.Address)
"cannot use %q as address. File already exists and is not socket", address)
}
}
case TCPListener, UDPListener:
default:
return fmt.Errorf("invalid listener type %q", s.Listener)
return fmt.Errorf("invalid listener type %q", listener)
}
return nil
}
Expand Down
106 changes: 106 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2026 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package config

import (
"path/filepath"
"testing"
)

func validSyslog(t *testing.T) Syslog {
t.Helper()
return Syslog{
Listener: UnixDgramListener,
Address: filepath.Join(t.TempDir(), "syslog.sock"),
Format: "automatic",
DataStore: StdOutDataStore,
}
}

func TestSyslogValidateExtraListener(t *testing.T) {
t.Parallel()

t.Run("primary only", func(t *testing.T) {
cfg := validSyslog(t)
if err := cfg.Validate(); err != nil {
t.Fatalf("expected valid config, got %v", err)
}
})

t.Run("unix and tcp", func(t *testing.T) {
cfg := validSyslog(t)
cfg.ExtraListener = TCPListener
cfg.ExtraAddress = "127.0.0.1:5144"
if err := cfg.Validate(); err != nil {
t.Fatalf("expected valid unix+tcp config, got %v", err)
}
})

t.Run("unix and udp", func(t *testing.T) {
cfg := validSyslog(t)
cfg.ExtraListener = UDPListener
cfg.ExtraAddress = "127.0.0.1:5144"
if err := cfg.Validate(); err != nil {
t.Fatalf("expected valid unix+udp config, got %v", err)
}
})

t.Run("tcp primary with unix extra", func(t *testing.T) {
cfg := validSyslog(t)
cfg.Listener = TCPListener
cfg.Address = "0.0.0.0:5144"
cfg.ExtraListener = UnixDgramListener
cfg.ExtraAddress = filepath.Join(t.TempDir(), "extra.sock")
if err := cfg.Validate(); err != nil {
t.Fatalf("expected valid tcp+unix config, got %v", err)
}
})

t.Run("extra listener without address", func(t *testing.T) {
cfg := validSyslog(t)
cfg.ExtraListener = TCPListener
if err := cfg.Validate(); err == nil {
t.Fatal("expected error when extra_address is missing")
}
})

t.Run("extra address without listener", func(t *testing.T) {
cfg := validSyslog(t)
cfg.ExtraAddress = "127.0.0.1:5144"
if err := cfg.Validate(); err == nil {
t.Fatal("expected error when extra_listener is missing")
}
})

t.Run("duplicate listener", func(t *testing.T) {
cfg := validSyslog(t)
cfg.Listener = TCPListener
cfg.Address = "127.0.0.1:5144"
cfg.ExtraListener = TCPListener
cfg.ExtraAddress = "127.0.0.1:5144"
if err := cfg.Validate(); err == nil {
t.Fatal("expected error when extra listener duplicates the primary")
}
})

t.Run("invalid extra listener", func(t *testing.T) {
cfg := validSyslog(t)
cfg.ExtraListener = "sctp"
cfg.ExtraAddress = "127.0.0.1:5144"
if err := cfg.Validate(); err == nil {
t.Fatal("expected error for invalid extra_listener")
}
})
}
Loading
Loading