feat: add "/api/v1/traces" API to receive OpenClaw's OpenTelemetry metric (#5349)

This commit is contained in:
Modo
2026-04-01 12:13:44 +08:00
committed by GitHub
parent 0ff862dbc5
commit 2ebe3f1d5d
8 changed files with 113 additions and 0 deletions

View File

@@ -85,6 +85,7 @@ p, *, *, POST, /api/send-verification-code, *, *
p, *, *, GET, /api/get-captcha, *, *
p, *, *, POST, /api/verify-captcha, *, *
p, *, *, POST, /api/verify-code, *, *
p, *, *, POST, /api/v1/traces, *, *
p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, *

View File

@@ -0,0 +1,77 @@
// Copyright 2026 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 controllers
import (
"fmt"
"io"
"strings"
"github.com/casdoor/casdoor/object"
coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
// AddOtlpEntry
// @Title AddTrace
// @Tag OTLP API
// @Description receive otlp trace protobuf
// @Success 200 {object} string
// @router /api/v1/traces [post]
func (c *ApiController) AddTrace() {
if !strings.HasPrefix(c.Ctx.Input.Header("Content-Type"), "application/x-protobuf") {
c.Ctx.Output.SetStatus(415)
c.Ctx.Output.Body([]byte("unsupported content type"))
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.Ctx.Output.SetStatus(400)
c.Ctx.Output.Body([]byte("read body failed"))
return
}
var req coltracepb.ExportTraceServiceRequest
if err := proto.Unmarshal(body, &req); err != nil {
c.Ctx.Output.SetStatus(400)
c.Ctx.Output.Body([]byte(fmt.Sprintf("bad protobuf: %v", err)))
return
}
message, err := protojson.Marshal(&req)
if err != nil {
c.Ctx.Output.SetStatus(500)
c.Ctx.Output.Body([]byte(fmt.Sprintf("marshal trace failed: %v", err)))
return
}
entry := object.NewTraceEntry(message)
if _, err := object.AddEntry(entry); err != nil {
c.Ctx.Output.SetStatus(500)
c.Ctx.Output.Body([]byte(fmt.Sprintf("save trace failed: %v", err)))
return
}
resp := &coltracepb.ExportTraceServiceResponse{}
respBytes, _ := proto.Marshal(resp)
c.Ctx.Output.Header("Content-Type", "application/x-protobuf")
c.Ctx.Output.SetStatus(200)
c.Ctx.Output.Body(respBytes)
}

2
go.mod
View File

@@ -80,6 +80,7 @@ require (
github.com/xorm-io/builder v0.3.13
github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6
go.opentelemetry.io/proto/otlp v1.7.1
golang.org/x/crypto v0.47.0
golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.34.0
@@ -186,6 +187,7 @@ require (
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gregdel/pushover v1.3.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect

4
go.sum
View File

@@ -1280,6 +1280,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -1855,6 +1857,8 @@ go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTq
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=

View File

@@ -31,6 +31,21 @@ type Entry struct {
Url string `xorm:"varchar(500)" json:"url"`
Token string `xorm:"varchar(500)" json:"token"`
Application string `xorm:"varchar(100)" json:"application"`
Message string `xorm:"mediumtext" json:"message"`
}
func NewTraceEntry(message []byte) *Entry {
currentTime := util.GetCurrentTime()
traceId := fmt.Sprintf("trace_%s_%s", util.GenerateSimpleTimeId(), util.GetRandomName())
return &Entry{
Owner: CasdoorOrganization,
Name: traceId,
CreatedTime: currentTime,
UpdatedTime: currentTime,
DisplayName: traceId,
Message: string(message),
}
}
func GetEntries(owner string) ([]*Entry, error) {

View File

@@ -146,6 +146,8 @@ func InitAPI() {
web.Router("/api/add-entry", &controllers.ApiController{}, "POST:AddEntry")
web.Router("/api/delete-entry", &controllers.ApiController{}, "POST:DeleteEntry")
web.Router("/api/v1/traces", &controllers.ApiController{}, "POST:AddTrace")
web.Router("/api/get-global-sites", &controllers.ApiController{}, "GET:GetGlobalSites")
web.Router("/api/get-sites", &controllers.ApiController{}, "GET:GetSites")
web.Router("/api/get-site", &controllers.ApiController{}, "GET:GetSite")

View File

@@ -22,6 +22,7 @@ import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend";
const {Option} = Select;
const {TextArea} = Input;
class EntryEditPage extends React.Component {
constructor(props) {
@@ -208,6 +209,16 @@ class EntryEditPage extends React.Component {
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("payment:Message")}:
</Col>
<Col span={22} >
<TextArea autoSize={{minRows: 8, maxRows: 20}} value={this.state.entry.message} onChange={e => {
this.updateEntryField("message", e.target.value);
}} />
</Col>
</Row>
</Card>
);
}

View File

@@ -34,6 +34,7 @@ class EntryListPage extends BaseListPage {
url: "",
token: "",
application: "",
message: "",
};
}