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
1 change: 1 addition & 0 deletions config.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@
|---|-----------|----|-------------|
|address|Listener address|`int`|`127.0.0.1`
|enabled|Enables the monitoring APIs|`boolean`|`false`
|loggingPath|The path at which to serve the dynamic logging API, which allows the log level to be changed at runtime|`string`|`/logging`
|metricsPath|The path from which to serve the Prometheus metrics|`string`|`/metrics`
|port|Listener port|`int`|`6000`
|publicURL|Externally available URL for the HTTP endpoint|`string`|`<nil>`
Expand Down
2 changes: 2 additions & 0 deletions internal/tmconfig/tmconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ var (
DeprecatedMetricsPath = ffc("metrics.path")
MonitoringEnabled = ffc("monitoring.enabled")
MonitoringMetricsPath = ffc("monitoring.metricsPath")
MonitoringLoggingPath = ffc("monitoring.loggingPath")
TransactionsHandlerName = ffc("transactions.handler.name")
TransactionsMaxHistoryCount = ffc("transactions.maxHistoryCount")
TransactionsNonceStateTimeout = ffc("transactions.nonceStateTimeout")
Expand Down Expand Up @@ -130,6 +131,7 @@ func setDefaults() {

viper.SetDefault(string(MonitoringEnabled), false)
viper.SetDefault(string(MonitoringMetricsPath), "/metrics")
viper.SetDefault(string(MonitoringLoggingPath), "/logging")

viper.SetDefault(string(APIPassthroughHeaders), []string{})
viper.SetDefault(string(DeprecatedPolicyEngineName), "simple")
Expand Down
1 change: 1 addition & 0 deletions internal/tmmsgs/en_config_descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ var (
DeprecatedConfigMetricsPath = ffc("config.metrics.path", "Deprecated: Please use 'monitoring.metricsPath' instead", i18n.StringType)
ConfigMonitoringEnabled = ffc("config.monitoring.enabled", "Enables the monitoring APIs", i18n.BooleanType)
ConfigMonitoringMetricsPath = ffc("config.monitoring.metricsPath", "The path from which to serve the Prometheus metrics", i18n.StringType)
ConfigMonitoringLoggingPath = ffc("config.monitoring.loggingPath", "The path at which to serve the dynamic logging API, which allows the log level to be changed at runtime", i18n.StringType)
ConfigMetricsPort = ffc("config.metrics.port", "The port on which the metrics HTTP API should listen", i18n.IntType)
ConfigMetricsPublicURL = ffc("config.metrics.publicURL", "The fully qualified public URL for the metrics API. This is used for building URLs in HTTP responses and in OpenAPI Spec generation", "URL "+i18n.StringType)
ConfigMetricsReadTimeout = ffc("config.metrics.readTimeout", "The maximum time to wait when reading from an HTTP connection", i18n.TimeDurationType)
Expand Down
26 changes: 26 additions & 0 deletions pkg/fftm/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ package fftm

import (
"net/http"
"strings"

"github.com/gorilla/mux"
"github.com/hyperledger-firefly/common/pkg/config"
"github.com/hyperledger-firefly/common/pkg/ffapi"
"github.com/hyperledger-firefly/common/pkg/i18n"
"github.com/hyperledger-firefly/common/pkg/log"
"github.com/hyperledger-firefly/transaction-manager/internal/tmconfig"
)

Expand Down Expand Up @@ -77,6 +79,7 @@ func (m *manager) createMonitoringMuxRouter() *mux.Router {
DefaultRequestTimeout: config.GetDuration(tmconfig.APIDefaultRequestTimeout),
MaxTimeout: config.GetDuration(tmconfig.APIMaxRequestTimeout),
}
r.Path(config.GetString(tmconfig.MonitoringLoggingPath)).Handler(hf.APIWrapper(m.loggingSettingsHandler))
for _, route := range m.monitoringRoutes() {
r.Path(route.Path).Methods(route.Method).Handler(hf.RouteHandler(route))
}
Expand All @@ -86,6 +89,29 @@ func (m *manager) createMonitoringMuxRouter() *mux.Router {
return r
}

// loggingSettingsHandler allows the log level to be changed dynamically at runtime,
// mirroring the standard monitoring API provided by ffapi.APIServer
func (m *manager) loggingSettingsHandler(_ http.ResponseWriter, req *http.Request) (status int, err error) {
if req.Method != http.MethodPut {
return http.StatusMethodNotAllowed, i18n.NewError(req.Context(), i18n.MsgMethodNotAllowed)
}
logLevel := req.URL.Query().Get("level")
if logLevel != "" {
l := log.L(log.WithLogFieldsMap(req.Context(), map[string]string{"new_level": logLevel}))
switch strings.ToLower(logLevel) {
case "error", "debug", "trace", "info", "warn":
// noop - all valid levels
default:
l.Warn("invalid log level")
return http.StatusBadRequest, i18n.NewError(req.Context(), i18n.MsgInvalidLogLevel, logLevel)
}
l.Warnf("changing log level to %s", logLevel)
log.SetLevel(logLevel)
}

return http.StatusAccepted, nil
}

func (m *manager) runAPIServer() {
m.apiServer.ServeHTTP(m.ctx)
}
Expand Down
68 changes: 68 additions & 0 deletions pkg/fftm/route_put_logging_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright © 2025 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// 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 fftm

import (
"fmt"
"testing"

"github.com/go-resty/resty/v2"
"github.com/hyperledger-firefly/common/pkg/log"
"github.com/hyperledger-firefly/transaction-manager/mocks/ffcapimocks"
"github.com/hyperledger-firefly/transaction-manager/pkg/ffcapi"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestPutLogging(t *testing.T) {
_, m, done := newTestManagerWithMetrics(t, false)
defer done()

require.True(t, m.monitoringEnabled)
url := fmt.Sprintf("http://%s", m.monitoringServer.Addr())

mfc := m.connector.(*ffcapimocks.API)
mfc.On("IsLive", mock.Anything).Return(&ffcapi.LiveResponse{Up: true}, ffcapi.ErrorReason(""), nil).Maybe()

err := m.Start()
assert.NoError(t, err)

defer log.SetLevel("info")

// Valid level change
res, err := resty.New().R().Put(url + "/logging?level=debug")
assert.NoError(t, err)
assert.Equal(t, 200, res.StatusCode())
assert.Equal(t, logrus.DebugLevel, logrus.GetLevel())

// No level supplied is a no-op
res, err = resty.New().R().Put(url + "/logging")
assert.NoError(t, err)
assert.Equal(t, 200, res.StatusCode())

// Invalid level
res, err = resty.New().R().Put(url + "/logging?level=wrong")
assert.NoError(t, err)
assert.Equal(t, 400, res.StatusCode())

// Wrong method
res, err = resty.New().R().Get(url + "/logging")
assert.NoError(t, err)
assert.Equal(t, 405, res.StatusCode())
}