Skip to content
Open
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ OTEL_METRIC_EXPORT_INTERVAL=60000
OTEL_BATCH_EXPORT_TIMEOUT=10000
OTEL_METRIC_EXPORT_TIMEOUT=30000

# prometheus metrics enabled
PROMETHEUS_ENABLED=true

# FORCE_VERIFYING_SIGNATURE, for security, you should set this to true, pls be sure you know what you are doing
# if want to install plugin without verifying signature, set this to false
FORCE_VERIFYING_SIGNATURE=true
Expand Down
11 changes: 9 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/hashicorp/go-version v1.7.0
github.com/langgenius/dify-cloud-kit v0.1.1
github.com/panjf2000/ants/v2 v2.10.0
github.com/prometheus/client_golang v1.23.2
github.com/redis/go-redis/extra/redisotel/v9 v9.17.3
github.com/redis/go-redis/v9 v9.17.3
github.com/spf13/cobra v1.8.1
Expand All @@ -25,6 +26,7 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0
go.opentelemetry.io/otel/sdk v1.39.0
go.opentelemetry.io/otel/sdk/metric v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
golang.org/x/tools v0.38.0
gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.30.0
Expand Down Expand Up @@ -70,6 +72,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down Expand Up @@ -109,7 +112,7 @@ require (
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
Expand All @@ -118,11 +121,15 @@ require (
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.17.3 // indirect
Expand All @@ -146,8 +153,8 @@ require (
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/time v0.12.0 // indirect
Expand Down
18 changes: 16 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
Expand Down Expand Up @@ -250,8 +252,8 @@ github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
Expand Down Expand Up @@ -297,6 +299,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/panjf2000/ants/v2 v2.10.0 h1:zhRg1pQUtkyRiOFo2Sbqwjp0GfBNo9cUY2/Grpx1p+8=
github.com/panjf2000/ants/v2 v2.10.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I=
github.com/panjf2000/gnet/v2 v2.5.5 h1:H+LqGgCHs2mGJq/4n6YELhMjZ027bNgd5Qb8Wj5nbrM=
Expand All @@ -319,6 +323,14 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
Expand Down Expand Up @@ -446,6 +458,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand Down
107 changes: 107 additions & 0 deletions internal/core/control_panel/metrics_notifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package controlpanel

import (
"github.com/langgenius/dify-plugin-daemon/internal/core/debugging_runtime"
"github.com/langgenius/dify-plugin-daemon/internal/core/local_runtime"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/metrics"
)

type MetricsNotifier struct{}

func NewMetricsNotifier() *MetricsNotifier {
return &MetricsNotifier{}
}

// identifiableRuntime is an interface for types that can provide their identity
type identifiableRuntime interface {
Identity() (plugin_entities.PluginUniqueIdentifier, error)
}

func (m *MetricsNotifier) OnLocalRuntimeStarting(pluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier) {
pluginID := pluginUniqueIdentifier.PluginID()
metrics.PluginRuntimeStatus.WithLabelValues(
pluginID,
"local",
).Set(0.5)
}

func (m *MetricsNotifier) OnLocalRuntimeReady(runtime *local_runtime.LocalPluginRuntime) {
pluginID := pluginIDFromRuntime(runtime)
metrics.PluginRuntimeStatus.WithLabelValues(
pluginID,
"local",
).Set(1)
metrics.ActivePluginsTotal.WithLabelValues("local").Inc()
}

func (m *MetricsNotifier) OnLocalRuntimeStartFailed(pluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier, err error) {
pluginID := pluginUniqueIdentifier.PluginID()
metrics.PluginRuntimeStatus.WithLabelValues(
pluginID,
"local",
).Set(0)
metrics.PluginInstallationsTotal.WithLabelValues(
pluginID,
"failed",
).Inc()
}

func (m *MetricsNotifier) OnLocalRuntimeStopped(runtime *local_runtime.LocalPluginRuntime) {
pluginID := pluginIDFromRuntime(runtime)
metrics.PluginRuntimeStatus.WithLabelValues(
pluginID,
"local",
).Set(0)
metrics.ActivePluginsTotal.WithLabelValues("local").Dec()
}

func (m *MetricsNotifier) OnLocalRuntimeStop(runtime *local_runtime.LocalPluginRuntime) {
pluginID := pluginIDFromRuntime(runtime)
metrics.PluginRuntimeStatus.WithLabelValues(
pluginID,
"local",
).Set(0)
}

func (m *MetricsNotifier) OnLocalRuntimeScaleUp(runtime *local_runtime.LocalPluginRuntime, i int32) {
}

func (m *MetricsNotifier) OnLocalRuntimeScaleDown(runtime *local_runtime.LocalPluginRuntime, i int32) {
}

func (m *MetricsNotifier) OnLocalRuntimeInstanceLog(
runtime *local_runtime.LocalPluginRuntime,
instance *local_runtime.PluginInstance,
event plugin_entities.PluginLogEvent,
) {
}

func (m *MetricsNotifier) OnDebuggingRuntimeConnected(runtime *debugging_runtime.RemotePluginRuntime) {
pluginID := pluginIDFromRuntime(runtime)
metrics.PluginRuntimeStatus.WithLabelValues(
pluginID,
"remote",
).Set(1)
metrics.ActivePluginsTotal.WithLabelValues("remote").Inc()
}

func (m *MetricsNotifier) OnDebuggingRuntimeDisconnected(runtime *debugging_runtime.RemotePluginRuntime) {
pluginID := pluginIDFromRuntime(runtime)
metrics.PluginRuntimeStatus.WithLabelValues(
pluginID,
"remote",
).Set(0)
metrics.ActivePluginsTotal.WithLabelValues("remote").Dec()
}

// pluginIDFromRuntime extracts the plugin ID from any runtime that implements identifiableRuntime
func pluginIDFromRuntime(runtime identifiableRuntime) string {
if runtime == nil {
return "unknown"
}
if identity, err := runtime.Identity(); err == nil {
return identity.PluginID()
}
return "unknown"
}
127 changes: 127 additions & 0 deletions internal/core/control_panel/metrics_notifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package controlpanel

import (
"errors"
"testing"

"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
"github.com/stretchr/testify/assert"
)

func TestPluginIDFromIdentifier(t *testing.T) {
tests := []struct {
name string
identifier string
expected string
}{
{
name: "standard plugin identifier",
identifier: "langgenius/openai:1.0.0@abc123def456789abc123def456789abcd",
expected: "langgenius/openai",
},
{
name: "plugin without author",
identifier: "openai:1.0.0@abc123def456789abc123def456789abcd",
expected: "openai",
},
{
name: "complex plugin identifier",
identifier: "author/my-plugin:2.1.3-beta@1234567890abcdef1234567890abcdef",
expected: "author/my-plugin",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
identifier, err := plugin_entities.NewPluginUniqueIdentifier(tt.identifier)
assert.NoError(t, err)

result := identifier.PluginID()
assert.Equal(t, tt.expected, result)
})
}
}

func TestNewMetricsNotifier(t *testing.T) {
notifier := NewMetricsNotifier()
assert.NotNil(t, notifier)

assert.IsType(t, &MetricsNotifier{}, notifier, "NewMetricsNotifier should return *MetricsNotifier type")
}

func TestMetricsNotifier_OnLocalRuntimeStarting(t *testing.T) {
notifier := NewMetricsNotifier()

identifier, err := plugin_entities.NewPluginUniqueIdentifier("langgenius/openai:1.0.0@abc123def456789abc123def456789abcd")
assert.NoError(t, err)

// This should not panic
assert.NotPanics(t, func() {
notifier.OnLocalRuntimeStarting(identifier)
})
}

func TestMetricsNotifier_OnLocalRuntimeStartFailed(t *testing.T) {
notifier := NewMetricsNotifier()

identifier, err := plugin_entities.NewPluginUniqueIdentifier("langgenius/openai:1.0.0@abc123def456789abc123def456789abcd")
assert.NoError(t, err)

// This should not panic
assert.NotPanics(t, func() {
notifier.OnLocalRuntimeStartFailed(identifier, assert.AnError)
})
}

func TestMetricsNotifier_ScaleEvents(t *testing.T) {
notifier := NewMetricsNotifier()

// Create a mock runtime (we can't easily create a real one in tests)
// but we can test that the methods don't panic
assert.NotPanics(t, func() {
// These methods have empty implementations currently
// but we test they exist and don't panic
type mockRuntime struct{}
notifier.OnLocalRuntimeScaleUp(nil, 1)
notifier.OnLocalRuntimeScaleDown(nil, 1)
})
}

func TestPluginIDFromRuntime(t *testing.T) {
t.Run("nil runtime", func(t *testing.T) {
result := pluginIDFromRuntime(nil)
assert.Equal(t, "unknown", result)
})

t.Run("mock runtime with successful identity", func(t *testing.T) {
identifier, _ := plugin_entities.NewPluginUniqueIdentifier("test-plugin:1.0.0@abc123def456789abc123def456789abcd")

mockRuntime := &mockIdentifiableRuntime{
identity: identifier,
err: nil,
}

result := pluginIDFromRuntime(mockRuntime)
assert.Equal(t, "test-plugin", result)
})

t.Run("mock runtime with error", func(t *testing.T) {
mockRuntime := &mockIdentifiableRuntime{
identity: "",
err: errors.New("identity error"),
}

result := pluginIDFromRuntime(mockRuntime)
assert.Equal(t, "unknown", result)
})
}

// mockIdentifiableRuntime is a mock implementation of identifiableRuntime for testing
type mockIdentifiableRuntime struct {
identity plugin_entities.PluginUniqueIdentifier
err error
}

func (m *mockIdentifiableRuntime) Identity() (plugin_entities.PluginUniqueIdentifier, error) {
return m.identity, m.err
}
1 change: 1 addition & 0 deletions internal/core/plugin_manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func InitGlobalManager(oss oss.OSS, config *app.Config) *PluginManager {
// mount control panel notifiers
manager.controlPanel.AddNotifier(&controlpanel.StandardLogger{})
manager.controlPanel.AddNotifier(&install_service.InstallListener{})
manager.controlPanel.AddNotifier(controlpanel.NewMetricsNotifier())

return manager
}
Expand Down
9 changes: 9 additions & 0 deletions internal/server/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/langgenius/dify-plugin-daemon/internal/service"
"github.com/langgenius/dify-plugin-daemon/internal/types/app"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/log"
"github.com/prometheus/client_golang/prometheus/promhttp"

sentrygin "github.com/getsentry/sentry-go/gin"
)
Expand All @@ -36,13 +37,21 @@ engine := gin.New()
engine.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"code": "not_found", "message": "route not found"})
})
if config.PrometheusEnabled {
engine.Use(PrometheusMiddleware())
}
engine.GET("/health/check", controllers.HealthCheck(config))

endpointGroup := engine.Group("/e")
serverlessTransactionGroup := engine.Group("/backwards-invocation")
pluginGroup := engine.Group("/plugin/:tenant_id")
pprofGroup := engine.Group("/debug/pprof")

if config.PrometheusEnabled {
metricsGroup := engine.Group("/metrics")
metricsGroup.GET("/", gin.WrapH(promhttp.Handler()))
}

if config.AdminApiEnabled {
if len(config.AdminApiKey) < 10 {
log.Panic("length of admin api key must be greater than 10")
Expand Down
Loading