feat: add Log provider interface and Linux syslog Log provider

Agent-Logs-Url: https://github.com/casdoor/casdoor/sessions/0d21baac-0d50-4061-8fcd-70f79b4dce94

Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-02 15:07:34 +00:00
committed by GitHub
parent f3fc41e03e
commit bb35137dd7
20 changed files with 299 additions and 12 deletions

2
go.mod
View File

@@ -87,6 +87,7 @@ require (
golang.org/x/text v0.33.0
golang.org/x/time v0.8.0
google.golang.org/api v0.215.0
google.golang.org/protobuf v1.36.11
layeh.com/radius v0.0.0-20231213012653-1006025d24f8
maunium.net/go/mautrix v0.22.1
modernc.org/sqlite v1.18.2
@@ -300,7 +301,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect

27
log/provider.go Normal file
View File

@@ -0,0 +1,27 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// 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 log
type LogProvider interface {
WriteLog(context string) error
}
func GetLogProvider(typ string, host string, port int, title string, method string) (LogProvider, error) {
if typ == "Linux Syslog" {
return NewSyslogProvider(host, port, title, method)
}
return nil, nil
}

51
log/syslog.go Normal file
View File

@@ -0,0 +1,51 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// 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.
//go:build !windows
package log
import (
"fmt"
golog "log/syslog"
)
type SyslogProvider struct {
writer *golog.Writer
}
func NewSyslogProvider(host string, port int, tag string, network string) (*SyslogProvider, error) {
var writer *golog.Writer
var err error
if host == "" {
writer, err = golog.New(golog.LOG_INFO|golog.LOG_USER, tag)
} else {
if port <= 0 {
port = 514
}
addr := fmt.Sprintf("%s:%d", host, port)
writer, err = golog.Dial(network, addr, golog.LOG_INFO|golog.LOG_USER, tag)
}
if err != nil {
return nil, err
}
return &SyslogProvider{writer: writer}, nil
}
func (s *SyslogProvider) WriteLog(context string) error {
return s.writer.Info(context)
}

29
log/syslog_windows.go Normal file
View File

@@ -0,0 +1,29 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// 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.
//go:build windows
package log
import "fmt"
func NewSyslogProvider(host string, port int, tag string, network string) (*SyslogProvider, error) {
return nil, fmt.Errorf("syslog is not supported on Windows")
}
type SyslogProvider struct{}
func (s *SyslogProvider) WriteLog(context string) error {
return fmt.Errorf("syslog is not supported on Windows")
}

51
object/log_provider.go Normal file
View File

@@ -0,0 +1,51 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// 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 object
import (
"fmt"
"github.com/casdoor/casdoor/log"
"github.com/casdoor/casdoor/util"
)
func SendLog(provider *Provider, record *Record) error {
logProvider, err := log.GetLogProvider(provider.Type, provider.Host, provider.Port, provider.Title, provider.Method)
if err != nil {
return err
}
if logProvider == nil {
return nil
}
return logProvider.WriteLog(util.StructToJson(record))
}
func AddRecordToLogProviders(record *Record) error {
providers, err := GetProvidersByCategory(record.Organization, "Log")
if err != nil {
return err
}
for _, provider := range providers {
err = SendLog(provider, record)
if err != nil {
fmt.Println(err)
}
}
return nil
}

View File

@@ -160,6 +160,11 @@ func AddRecord(record *Record) bool {
fmt.Println(errWebhook)
}
errLog := AddRecordToLogProviders(record)
if errLog != nil {
fmt.Println(errLog)
}
affected, err := addRecord(record)
if err != nil {
panic(err)

View File

@@ -32,6 +32,7 @@ import {renderWeb3ProviderFields} from "./provider/Web3ProviderFields";
import {renderStorageProviderFields} from "./provider/StorageProviderFields";
import {renderFaceIdProviderFields} from "./provider/FaceIDProviderFields";
import {renderIDVerificationProviderFields} from "./provider/IDVerificationProviderFields";
import {renderLogProviderFields} from "./provider/LogProviderFields";
const {Option} = Select;
const {TextArea} = Input;
@@ -715,6 +716,8 @@ class ProviderEditPage extends React.Component {
} else if (value === "ID Verification") {
this.updateProviderField("type", "Jumio");
this.updateProviderField("endpoint", "");
} else if (value === "Log") {
this.updateProviderField("type", "Linux Syslog");
}
})}>
{
@@ -722,6 +725,7 @@ class ProviderEditPage extends React.Component {
{id: "Captcha", name: "Captcha"},
{id: "Email", name: "Email"},
{id: "ID Verification", name: "ID Verification"},
{id: "Log", name: "Log"},
{id: "MFA", name: "MFA"},
{id: "Notification", name: "Notification"},
{id: "OAuth", name: "OAuth"},
@@ -851,6 +855,7 @@ class ProviderEditPage extends React.Component {
(this.state.provider.category === "Captcha" && this.state.provider.type === "Default") ||
(this.state.provider.category === "Web3") ||
(this.state.provider.category === "MFA") ||
(this.state.provider.category === "Log") ||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
(this.state.provider.category === "Email" && this.state.provider.type === "Custom HTTP Email") ||
@@ -975,6 +980,10 @@ class ProviderEditPage extends React.Component {
this.state.provider,
this.updateProviderField.bind(this)
) : null}
{this.state.provider.category === "Log" ? renderLogProviderFields(
this.state.provider,
this.updateProviderField.bind(this)
) : null}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Provider URL"), i18next.t("provider:Provider URL - Tooltip"))} :

View File

@@ -1419,6 +1419,10 @@ export function getProviderTypeOptions(category) {
{id: "Jumio", name: "Jumio"},
{id: "Alibaba Cloud", name: "Alibaba Cloud"},
]);
} else if (category === "Log") {
return ([
{id: "Linux Syslog", name: "Linux Syslog"},
]);
} else {
return [];
}

View File

@@ -1165,7 +1165,10 @@
"Wallets": "Brieftaschen",
"Wallets - Tooltip": "Unterstützte digitale Brieftaschen",
"Web": "Web",
"admin (Shared)": "admin (Gemeinsam)"
"admin (Shared)": "admin (Gemeinsam)",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "Wurde ausgelöst",

View File

@@ -1165,7 +1165,10 @@
"Wallets": "Wallets",
"Wallets - Tooltip": "Supported digital wallets",
"Web": "Web",
"admin (Shared)": "admin (Shared)"
"admin (Shared)": "admin (Shared)",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "Is triggered",

View File

@@ -1165,7 +1165,10 @@
"Wallets": "Billeteras",
"Wallets - Tooltip": "Billeteras - Información adicional",
"Web": "Web",
"admin (Shared)": "administrador (compartido)"
"admin (Shared)": "administrador (compartido)",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "Fue activado",

View File

@@ -1165,7 +1165,10 @@
"Wallets": "Portefeuilles",
"Wallets - Tooltip": "Portefeuilles - Infobulle",
"Web": "Web",
"admin (Shared)": "admin (Partagé)"
"admin (Shared)": "admin (Partagé)",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "Est déclenché",

View File

@@ -1165,7 +1165,10 @@
"Wallets": "ウォレット",
"Wallets - Tooltip": "ウォレット - ツールチップ",
"Web": "ウェブ",
"admin (Shared)": "管理者(共有)"
"admin (Shared)": "管理者(共有)",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "トリガーされました",

View File

@@ -1165,7 +1165,10 @@
"Wallets": "Portfele",
"Wallets - Tooltip": "Portfele - Podpowiedź",
"Web": "Web",
"admin (Shared)": "admin (Wspólny)"
"admin (Shared)": "admin (Wspólny)",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "Zostało wyzwolone",

View File

@@ -1165,7 +1165,10 @@
"Wallets": "Carteiras",
"Wallets - Tooltip": "Dica: carteiras",
"Web": "Web",
"admin (Shared)": "admin (Compartilhado)"
"admin (Shared)": "admin (Compartilhado)",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "Foi acionado",

View File

@@ -1165,7 +1165,10 @@
"Wallets": "Cüzdanlar",
"Wallets - Tooltip": "Cüzdanlar - Araç ipucu",
"Web": "Web",
"admin (Shared)": "yönetici (Paylaşılan)"
"admin (Shared)": "yönetici (Paylaşılan)",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "Tetiklendi mi",

View File

@@ -1165,7 +1165,10 @@
"Wallets": "Гаманці",
"Wallets - Tooltip": "Гаманці підказка",
"Web": "Web",
"admin (Shared)": "адміністратор (спільно)"
"admin (Shared)": "адміністратор (спільно)",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "Спрацьовує",

View File

@@ -1165,7 +1165,10 @@
"Wallets": "Ví",
"Wallets - Tooltip": "Gợi ý ví",
"Web": "Web",
"admin (Shared)": "quản trị viên (Chung)"
"admin (Shared)": "quản trị viên (Chung)",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "Đã kích hoạt",

View File

@@ -1165,7 +1165,10 @@
"Wallets": "钱包",
"Wallets - Tooltip": "支持的数字钱包",
"Web": "Web",
"admin (Shared)": "admin共享"
"admin (Shared)": "admin共享",
"Tag": "Tag",
"Tag - Tooltip": "The syslog tag (program name) to identify log messages",
"Leave empty to use local syslog": "Leave empty to use local syslog"
},
"record": {
"Is triggered": "是否触发",

View File

@@ -0,0 +1,78 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// 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.
import React from "react";
import {Col, Input, InputNumber, Row, Select} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
const {Option} = Select;
export function renderLogProviderFields(provider, updateProviderField) {
return (
<React.Fragment>
{provider.type === "Linux Syslog" ? (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.host} placeholder={i18next.t("provider:Leave empty to use local syslog")} onChange={e => {
updateProviderField("host", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={provider.port} min={0} max={65535} style={{width: "100%"}} onChange={value => {
updateProviderField("port", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Tag"), i18next.t("provider:Tag - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.title} onChange={e => {
updateProviderField("title", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={provider.method || "udp"} onChange={value => {
updateProviderField("method", value);
}}>
{
[
{id: "udp", name: "UDP"},
{id: "tcp", name: "TCP"},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
</React.Fragment>
) : null}
</React.Fragment>
);
}