Skip to content

Commit 98e406e

Browse files
committed
fix: detect the custom timezone and adjust last time activity (#504)
1 parent a875eb9 commit 98e406e

File tree

9 files changed

+154
-23
lines changed

9 files changed

+154
-23
lines changed

engine/Dockerfile.dblab-cli

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM docker:20.10.24
22

33
# Install dependencies.
4-
RUN apk update && apk add --no-cache bash jq
4+
RUN apk update && apk add --no-cache bash jq tzdata
55

66
WORKDIR /home/dblab
77
COPY ./bin/dblab ./bin/dblab

engine/Dockerfile.dblab-server

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ FROM docker:20.10.24
44

55
# Install dependencies
66
RUN apk update \
7-
&& apk add --no-cache zfs lvm2 bash util-linux
7+
&& apk add --no-cache zfs lvm2 bash util-linux tzdata
88

99
WORKDIR /home/dblab
1010

engine/Dockerfile.dblab-server-debug

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ FROM docker:20.10.12
1717
# Install dependencies
1818
RUN apk update \
1919
&& apk add zfs=2.1.4-r0 --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \
20-
&& apk add --no-cache lvm2 bash util-linux
20+
&& apk add --no-cache lvm2 bash util-linux tzdata
2121

2222
WORKDIR /home/dblab
2323

engine/Dockerfile.dblab-server-zfs08

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ FROM docker:20.10.24
44

55
# Install dependencies.
66
RUN apk update && apk add zfs=0.8.4-r0 --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/v3.12/main \
7-
&& apk add --no-cache lvm2 bash util-linux
7+
&& apk add --no-cache lvm2 bash util-linux tzdata
88

99
WORKDIR /home/dblab
1010

engine/internal/provision/databases/postgres/pgconfig/configuration.go

+16-6
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ func (m *Manager) adjustHBAConf() error {
224224
}
225225

226226
// adjustGeneralConfigs corrects general PostgreSQL parameters with Database Lab configs.
227-
func (m Manager) adjustGeneralConfigs() error {
227+
func (m *Manager) adjustGeneralConfigs() error {
228228
log.Dbg("Configuring Postgres...")
229229

230230
pgConfSrc, err := util.GetStandardConfigPath(path.Join(pgCfgDir, pgControlDir, PgConfName))
@@ -439,16 +439,16 @@ func (m *Manager) ApplyUserConfig(cfg map[string]string) error {
439439

440440
// getConfigPath builds a path of the Database Lab config file.
441441
func (m *Manager) getConfigPath(configName string) string {
442-
return path.Join(m.dataDir, configPrefix+configName)
442+
return GetConfigPath(m.dataDir, configName)
443443
}
444444

445445
// recoveryPath returns the path of the recovery configuration file.
446-
func (m Manager) recoveryPath() string {
446+
func (m *Manager) recoveryPath() string {
447447
return path.Join(m.dataDir, m.recoveryFilename())
448448
}
449449

450450
// recoveryFilename returns the name of the recovery configuration file.
451-
func (m Manager) recoveryFilename() string {
451+
func (m *Manager) recoveryFilename() string {
452452
if m.pgVersion >= defaults.PGVersion12 {
453453
return configPrefix + recoveryConfName
454454
}
@@ -457,12 +457,12 @@ func (m Manager) recoveryFilename() string {
457457
}
458458

459459
// recoverySignalPath returns the path of the recovery signal file.
460-
func (m Manager) recoverySignalPath() string {
460+
func (m *Manager) recoverySignalPath() string {
461461
return path.Join(m.dataDir, recoverySignal)
462462
}
463463

464464
// standbySignalPath returns the path of the standby signal file.
465-
func (m Manager) standbySignalPath() string {
465+
func (m *Manager) standbySignalPath() string {
466466
return path.Join(m.dataDir, standbySignal)
467467
}
468468

@@ -509,6 +509,16 @@ func (m *Manager) truncateConfig(pgConf string) error {
509509
return os.WriteFile(pgConf, []byte{}, 0644)
510510
}
511511

512+
// ReadUserConfig reads user configuration file.
513+
func ReadUserConfig(dataDir string) (map[string]string, error) {
514+
return readConfig(GetConfigPath(dataDir, userConfigName))
515+
}
516+
517+
// GetConfigPath returns configuration path.
518+
func GetConfigPath(dataDir, configName string) string {
519+
return path.Join(dataDir, configPrefix+configName)
520+
}
521+
512522
// readConfig reads a configuration file.
513523
func readConfig(cfgFile string) (map[string]string, error) {
514524
config := make(map[string]string)

engine/internal/provision/mode_local.go

+39-8
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/pkg/errors"
2525

2626
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/databases/postgres"
27+
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/databases/postgres/pgconfig"
2728
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/docker"
2829
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/pool"
2930
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/resources"
@@ -620,12 +621,15 @@ func (p *Provisioner) LastSessionActivity(session *resources.Session, minimumTim
620621
ctx, cancel := context.WithCancel(p.ctx)
621622
defer cancel()
622623

623-
fileSelector := pglog.NewSelector(fsm.Pool().ClonePath(session.Port))
624+
clonePath := fsm.Pool().ClonePath(session.Port)
625+
fileSelector := pglog.NewSelector(clonePath)
624626

625627
if err := fileSelector.DiscoverLogDir(); err != nil {
626628
return nil, errors.Wrap(err, "failed to init file selector")
627629
}
628630

631+
location := detectLogsTimeZone(clonePath)
632+
629633
fileSelector.SetMinimumTime(minimumTime)
630634
fileSelector.FilterOldFilesInList()
631635

@@ -639,7 +643,7 @@ func (p *Provisioner) LastSessionActivity(session *resources.Session, minimumTim
639643
return nil, errors.Wrap(err, "failed get CSV log filenames")
640644
}
641645

642-
activity, err := p.scanCSVLogFile(ctx, filename, minimumTime)
646+
activity, err := p.scanCSVLogFile(ctx, filename, minimumTime, location)
643647
if err == io.EOF {
644648
continue
645649
}
@@ -650,12 +654,39 @@ func (p *Provisioner) LastSessionActivity(session *resources.Session, minimumTim
650654
return nil, pglog.ErrNotFound
651655
}
652656

653-
const csvMessageLogFieldsLength = 14
657+
const (
658+
csvMessageLogFieldsLength = 14
659+
logTZ = "log_timezone"
660+
)
661+
662+
func detectLogsTimeZone(dataDir string) *time.Location {
663+
userCfg, err := pgconfig.ReadUserConfig(dataDir)
664+
if err != nil {
665+
log.Msg("unable to read user-defined config of clone:", err.Error())
666+
667+
return time.UTC
668+
}
669+
670+
if tz, ok := userCfg[logTZ]; ok {
671+
location, err := time.LoadLocation(tz)
672+
673+
if err != nil {
674+
log.Msg(fmt.Sprintf("unable to load location (%q) defined in config: %s", tz, err.Error()))
675+
676+
return time.UTC
677+
}
678+
679+
return location
680+
}
681+
682+
return time.UTC
683+
}
654684

655-
func (p *Provisioner) scanCSVLogFile(ctx context.Context, filename string, availableTime time.Time) (*time.Time, error) {
685+
func (p *Provisioner) scanCSVLogFile(ctx context.Context, filename string, availableTime time.Time,
686+
location *time.Location) (*time.Time, error) {
656687
csvFile, err := os.Open(filename)
657688
if err != nil {
658-
return nil, errors.Wrap(err, "failed to open a CSV log file")
689+
return nil, errors.Wrap(err, "failed to open CSV log file")
659690
}
660691

661692
defer func() {
@@ -683,13 +714,13 @@ func (p *Provisioner) scanCSVLogFile(ctx context.Context, filename string, avail
683714
logTime := entry[0]
684715
logMessage := entry[13]
685716

686-
lastActivity, err := pglog.ParsePostgresLastActivity(logTime, logMessage)
717+
lastActivity, err := pglog.ParsePostgresLastActivity(logTime, logMessage, location)
687718
if err != nil {
688-
return nil, errors.Wrapf(err, "failed to get the time of last activity")
719+
return nil, errors.Wrapf(err, "failed to determine last activity timestamp")
689720
}
690721

691722
// Filter invalid and non-recent activity.
692-
if lastActivity == nil || lastActivity.Before(availableTime) {
723+
if lastActivity == nil || lastActivity.In(time.UTC).Before(availableTime) {
693724
continue
694725
}
695726

engine/internal/provision/mode_local_test.go

+86
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package provision
22

33
import (
44
"context"
5+
"os"
6+
"path"
57
"testing"
68
"time"
79

@@ -240,3 +242,87 @@ func TestLatestSnapshot(t *testing.T) {
240242
}
241243
})
242244
}
245+
246+
func TestDetectLogsTimeZone(t *testing.T) {
247+
tempDir := path.Join(os.TempDir(), "dle_logs_tz")
248+
defer os.RemoveAll(tempDir)
249+
250+
const (
251+
layout = "2006-01-02 15:04:05.000 MST"
252+
datetime = "2023-04-28 12:50:10.779 CEST"
253+
emptyContent = `# PostgreSQL configuration file`
254+
invalidContent = `# PostgreSQL configuration file
255+
# TimeZone setting
256+
log_timezone = 'America/Stockholm'
257+
`
258+
validContent = `# PostgreSQL configuration file
259+
# TimeZone setting
260+
log_timezone = 'Europe/Stockholm'
261+
`
262+
)
263+
264+
tests := []struct {
265+
name string
266+
dataDir string
267+
fileName string
268+
content string
269+
expectedLoc *time.Location
270+
}{
271+
{
272+
name: "no config file",
273+
dataDir: "/path/to/missing/config",
274+
fileName: "missing_config",
275+
expectedLoc: time.UTC,
276+
},
277+
{
278+
name: "config file without timezone",
279+
dataDir: "empty_config",
280+
fileName: "postgresql.dblab.user_defined.conf",
281+
content: emptyContent,
282+
expectedLoc: time.UTC,
283+
},
284+
{
285+
name: "config file with invalid timezone",
286+
dataDir: "invalid_dir",
287+
fileName: "postgresql.dblab.user_defined.conf",
288+
content: invalidContent,
289+
expectedLoc: time.UTC,
290+
},
291+
{
292+
name: "config file with valid timezone",
293+
dataDir: "valid_dir",
294+
fileName: "postgresql.dblab.user_defined.conf",
295+
content: validContent,
296+
expectedLoc: time.FixedZone("CEST", 2*60*60), // CEST (+2)
297+
},
298+
}
299+
300+
for _, tt := range tests {
301+
t.Run(tt.name, func(t *testing.T) {
302+
testCaseDir := path.Join(tempDir, tt.dataDir)
303+
err := createTempConfigFile(testCaseDir, tt.fileName, tt.content)
304+
require.NoError(t, err)
305+
306+
loc := detectLogsTimeZone(testCaseDir)
307+
308+
expectedTime, err := time.ParseInLocation(layout, datetime, tt.expectedLoc)
309+
require.NoError(t, err)
310+
311+
locationTime, err := time.ParseInLocation(layout, datetime, loc)
312+
require.NoError(t, err)
313+
314+
require.Truef(t, locationTime.UTC().Equal(expectedTime.UTC()), "detectLogsTimeZone(%s) returned unexpected location time. Expected %s, but got %s.", tt.dataDir, expectedTime, locationTime)
315+
})
316+
}
317+
}
318+
319+
func createTempConfigFile(testCaseDir, fileName string, content string) error {
320+
err := os.MkdirAll(testCaseDir, 0777)
321+
if err != nil {
322+
return err
323+
}
324+
325+
fn := path.Join(testCaseDir, fileName)
326+
327+
return os.WriteFile(fn, []byte(content), 0666)
328+
}

engine/pkg/util/pglog/activity.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,12 @@ func (s *Selector) FilterOldFilesInList() {
124124
}
125125

126126
// ParsePostgresLastActivity extracts the time of last session activity.
127-
func ParsePostgresLastActivity(logTime, text string) (*time.Time, error) {
127+
func ParsePostgresLastActivity(logTime, text string, loc *time.Location) (*time.Time, error) {
128128
if logTime == "" || !(strings.Contains(text, "statement:") || strings.Contains(text, "duration:")) {
129129
return nil, nil
130130
}
131131

132-
lastActivityTime, err := time.Parse("2006-01-02 15:04:05.000 MST", logTime)
132+
lastActivityTime, err := time.ParseInLocation("2006-01-02 15:04:05.000 MST", logTime, loc)
133133
if err != nil {
134134
return nil, errs.Wrap(err, "failed to parse the last activity time")
135135
}

engine/pkg/util/pglog/activity_test.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,24 @@ func TestGetPostgresLastActivity(t *testing.T) {
1414
logTime string
1515
logMessage string
1616
timeActivity *time.Time
17+
loc *time.Location
1718
}{
1819
{
1920
logTime: "2020-01-10 11:49:14.615 UTC",
2021
logMessage: "duration: 9.893 ms statement: SELECT 1;",
22+
loc: time.UTC,
2123
timeActivity: pointer.ToTime(time.Date(2020, 1, 10, 11, 49, 14, 615000000, time.UTC)),
2224
},
2325
{
2426
logTime: "2020-01-10 11:49:14.615 CET",
2527
logMessage: "duration: 9.893 ms statement: SELECT 1;",
26-
timeActivity: pointer.ToTime(time.Date(2020, 1, 10, 11, 49, 14, 615000000, time.FixedZone("CET", 0))),
28+
loc: time.FixedZone("CET", 3600),
29+
timeActivity: pointer.ToTime(time.Date(2020, 1, 10, 11, 49, 14, 615000000, time.FixedZone("CET", 3600))),
2730
},
2831
{
2932
logTime: "2020-01-11 13:10:58.503 UTC",
3033
logMessage: "duration: 0.077 ms statement:",
34+
loc: time.UTC,
3135
timeActivity: pointer.ToTime(time.Date(2020, 1, 11, 13, 10, 58, 503000000, time.UTC)),
3236
},
3337
{
@@ -48,7 +52,7 @@ func TestGetPostgresLastActivity(t *testing.T) {
4852
}
4953

5054
for _, tc := range testCases {
51-
lastActivity, err := ParsePostgresLastActivity(tc.logTime, tc.logMessage)
55+
lastActivity, err := ParsePostgresLastActivity(tc.logTime, tc.logMessage, tc.loc)
5256
require.NoError(t, err)
5357
assert.Equal(t, tc.timeActivity, lastActivity)
5458
}
@@ -68,7 +72,7 @@ func TestGetPostgresLastActivityWhenFailedParseTime(t *testing.T) {
6872
}
6973

7074
for _, tc := range testCases {
71-
lastActivity, err := ParsePostgresLastActivity(tc.logTime, tc.logMessage)
75+
lastActivity, err := ParsePostgresLastActivity(tc.logTime, tc.logMessage, time.UTC)
7276
require.Nil(t, lastActivity)
7377
assert.EqualError(t, err, tc.errorString)
7478
}

0 commit comments

Comments
 (0)