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
4 changes: 4 additions & 0 deletions backend/plugins/q_dev/api/blueprint_v200.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ func makeDataSourcePipelinePlanV200(
ConnectionId: s3Slice.ConnectionId,
S3Prefix: s3Slice.Prefix,
ScopeId: s3Slice.Id,
AccountId: s3Slice.AccountId,
BasePath: s3Slice.BasePath,
Year: s3Slice.Year,
Month: s3Slice.Month,
}

// Pass empty entities array to enable all subtasks
Expand Down
21 changes: 21 additions & 0 deletions backend/plugins/q_dev/impl/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (p QDev) GetTablesInfo() []dal.Tabler {
&models.QDevUserData{},
&models.QDevS3FileMeta{},
&models.QDevS3Slice{},
&models.QDevUserReport{},
}
}

Expand Down Expand Up @@ -117,10 +118,30 @@ func (p QDev) PrepareTaskData(taskCtx plugin.TaskContext, options map[string]int
identityClient = nil
}

// Resolve S3 prefixes to scan
var s3Prefixes []string
if op.AccountId != "" {
// New-style scope: construct both report paths using region from connection
region := connection.Region
timePart := fmt.Sprintf("%04d", op.Year)
if op.Month != nil {
timePart = fmt.Sprintf("%04d/%02d", op.Year, *op.Month)
}
base := fmt.Sprintf("%s/AWSLogs/%s/KiroLogs", op.BasePath, op.AccountId)
s3Prefixes = []string{
fmt.Sprintf("%s/by_user_analytic/%s/%s", base, region, timePart),
fmt.Sprintf("%s/user_report/%s/%s", base, region, timePart),
}
} else {
// Legacy scope: use S3Prefix directly
s3Prefixes = []string{op.S3Prefix}
}

return &tasks.QDevTaskData{
Options: &op,
S3Client: s3Client,
IdentityClient: identityClient,
S3Prefixes: s3Prefixes,
}, nil
}

Expand Down
39 changes: 36 additions & 3 deletions backend/plugins/q_dev/impl/impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestQDev_BasicPluginMethods(t *testing.T) {

// Test table info
tables := plugin.GetTablesInfo()
assert.Len(t, tables, 4)
assert.Len(t, tables, 5)

// Test subtask metas
subtasks := plugin.SubTaskMetas()
Expand All @@ -48,7 +48,7 @@ func TestQDev_BasicPluginMethods(t *testing.T) {
}

func TestQDev_TaskDataStructure(t *testing.T) {
// Test that QDevTaskData has the expected structure
// Test that QDevTaskData has the expected structure (legacy mode)
taskData := &tasks.QDevTaskData{
Options: &tasks.QDevOptions{
ConnectionId: 1,
Expand All @@ -61,6 +61,7 @@ func TestQDev_TaskDataStructure(t *testing.T) {
StoreId: "d-1234567890",
Region: "us-west-2",
},
S3Prefixes: []string{"test/"},
}

assert.NotNil(t, taskData.Options)
Expand All @@ -72,6 +73,36 @@ func TestQDev_TaskDataStructure(t *testing.T) {
assert.Equal(t, "test-bucket", taskData.S3Client.Bucket)
assert.Equal(t, "d-1234567890", taskData.IdentityClient.StoreId)
assert.Equal(t, "us-west-2", taskData.IdentityClient.Region)
assert.Equal(t, []string{"test/"}, taskData.S3Prefixes)
}

func TestQDev_TaskDataWithAccountId(t *testing.T) {
// Test new-style scope with AccountId and multiple S3Prefixes
month := 1
taskData := &tasks.QDevTaskData{
Options: &tasks.QDevOptions{
ConnectionId: 1,
AccountId: "034362076319",
BasePath: "user-report",
Year: 2026,
Month: &month,
},
S3Client: &tasks.QDevS3Client{
Bucket: "test-bucket",
},
S3Prefixes: []string{
"user-report/AWSLogs/034362076319/KiroLogs/by_user_analytic/us-east-1/2026/01",
"user-report/AWSLogs/034362076319/KiroLogs/user_report/us-east-1/2026/01",
},
}

assert.Equal(t, "034362076319", taskData.Options.AccountId)
assert.Equal(t, "user-report", taskData.Options.BasePath)
assert.Equal(t, 2026, taskData.Options.Year)
assert.Equal(t, &month, taskData.Options.Month)
assert.Len(t, taskData.S3Prefixes, 2)
assert.Contains(t, taskData.S3Prefixes[0], "by_user_analytic")
assert.Contains(t, taskData.S3Prefixes[1], "user_report")
}

func TestQDev_TaskDataWithoutIdentityClient(t *testing.T) {
Expand All @@ -83,10 +114,12 @@ func TestQDev_TaskDataWithoutIdentityClient(t *testing.T) {
S3Client: &tasks.QDevS3Client{
Bucket: "test-bucket",
},
IdentityClient: nil, // No identity client
IdentityClient: nil,
S3Prefixes: []string{"some-prefix/"},
}

assert.NotNil(t, taskData.Options)
assert.NotNil(t, taskData.S3Client)
assert.Nil(t, taskData.IdentityClient)
assert.Len(t, taskData.S3Prefixes, 1)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 migrationscripts

import (
"github.com/apache/incubator-devlake/core/context"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/helpers/migrationhelper"
"github.com/apache/incubator-devlake/plugins/q_dev/models/migrationscripts/archived"
)

type addUserReportTable struct{}

func (*addUserReportTable) Up(basicRes context.BasicRes) errors.Error {
return migrationhelper.AutoMigrateTables(
basicRes,
&archived.QDevUserReport{},
)
}

func (*addUserReportTable) Version() uint64 {
return 20260219000001
}

func (*addUserReportTable) Name() string {
return "Add user_report table for Kiro credits/subscription metrics"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 migrationscripts

import (
"github.com/apache/incubator-devlake/core/context"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
)

var _ plugin.MigrationScript = (*addAccountIdToS3Slice)(nil)

type addAccountIdToS3Slice struct{}

func (*addAccountIdToS3Slice) Up(basicRes context.BasicRes) errors.Error {
db := basicRes.GetDal()

err := db.Exec(`
ALTER TABLE _tool_q_dev_s3_slices
ADD COLUMN IF NOT EXISTS account_id VARCHAR(255) DEFAULT NULL
`)
if err != nil {
// Try alternative syntax for databases that don't support IF NOT EXISTS
_ = db.Exec(`ALTER TABLE _tool_q_dev_s3_slices ADD COLUMN account_id VARCHAR(255) DEFAULT NULL`)
}

return nil
}

func (*addAccountIdToS3Slice) Version() uint64 {
return 20260220000001
}

func (*addAccountIdToS3Slice) Name() string {
return "add account_id column to _tool_q_dev_s3_slices for auto-constructing S3 prefixes"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 archived

import (
"time"

"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
)

type QDevUserReport struct {
archived.Model
ConnectionId uint64 `gorm:"primaryKey"`
UserId string `gorm:"index" json:"userId"`
Date time.Time `gorm:"index" json:"date"`
DisplayName string `gorm:"type:varchar(255)" json:"displayName"`
ScopeId string `gorm:"index;type:varchar(255)" json:"scopeId"`
ClientType string `gorm:"type:varchar(50)" json:"clientType"`
SubscriptionTier string `gorm:"type:varchar(50)" json:"subscriptionTier"`
ProfileId string `gorm:"type:varchar(512)" json:"profileId"`
ChatConversations int `json:"chatConversations"`
CreditsUsed float64 `json:"creditsUsed"`
OverageCap float64 `json:"overageCap"`
OverageCreditsUsed float64 `json:"overageCreditsUsed"`
OverageEnabled bool `json:"overageEnabled"`
TotalMessages int `json:"totalMessages"`
}

func (QDevUserReport) TableName() string {
return "_tool_q_dev_user_report"
}
2 changes: 2 additions & 0 deletions backend/plugins/q_dev/models/migrationscripts/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@ func All() []plugin.MigrationScript {
new(addS3SliceTable),
new(addScopeConfigIdToS3Slice),
new(addScopeIdFields),
new(addUserReportTable),
new(addAccountIdToS3Slice),
}
}
58 changes: 48 additions & 10 deletions backend/plugins/q_dev/models/s3_slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type QDevS3Slice struct {
Id string `json:"id" mapstructure:"id" gorm:"primaryKey;type:varchar(512)"`
Prefix string `json:"prefix" mapstructure:"prefix" gorm:"type:varchar(512);not null"`
BasePath string `json:"basePath" mapstructure:"basePath" gorm:"type:varchar(512)"`
AccountId string `json:"accountId,omitempty" mapstructure:"accountId" gorm:"type:varchar(255)"`
Year int `json:"year" mapstructure:"year" gorm:"not null"`
Month *int `json:"month,omitempty" mapstructure:"month"`

Expand Down Expand Up @@ -61,6 +62,7 @@ func (s *QDevS3Slice) normalize(strict bool) error {
}

s.BasePath = cleanPath(s.BasePath)
s.AccountId = strings.TrimSpace(s.AccountId)
s.Prefix = cleanPath(selectNonEmpty(s.Prefix, s.Id))

if s.Year <= 0 {
Expand All @@ -81,23 +83,37 @@ func (s *QDevS3Slice) normalize(strict bool) error {
}
}

if s.Prefix == "" {
s.Prefix = buildPrefix(s.BasePath, s.Year, s.Month)
}
if s.AccountId != "" {
// New-style scope: construct a logical identifier from component parts
s.Prefix = buildPrefixWithAccount(s.BasePath, s.AccountId, s.Year, s.Month)
} else {
// Legacy scope: derive prefix from basePath + year + month
if s.Prefix == "" {
s.Prefix = buildPrefix(s.BasePath, s.Year, s.Month)
}

prefix := buildPrefix(s.BasePath, s.Year, s.Month)
if prefix != "" {
s.Prefix = prefix
prefix := buildPrefix(s.BasePath, s.Year, s.Month)
if prefix != "" {
s.Prefix = prefix
}
}

if s.Id == "" {
s.Id = s.Prefix
}

if s.Month != nil {
s.Name = fmt.Sprintf("%04d-%02d", s.Year, *s.Month)
} else if s.Year > 0 {
s.Name = fmt.Sprintf("%04d", s.Year)
if s.AccountId != "" {
if s.Month != nil {
s.Name = fmt.Sprintf("%s %04d-%02d", s.AccountId, s.Year, *s.Month)
} else if s.Year > 0 {
s.Name = fmt.Sprintf("%s %04d", s.AccountId, s.Year)
}
} else {
if s.Month != nil {
s.Name = fmt.Sprintf("%04d-%02d", s.Year, *s.Month)
} else if s.Year > 0 {
s.Name = fmt.Sprintf("%04d", s.Year)
}
}

if s.FullName == "" {
Expand Down Expand Up @@ -150,6 +166,14 @@ func (s QDevS3Slice) ScopeName() string {
if s.Name != "" {
return s.Name
}
if s.AccountId != "" {
if s.Month != nil {
return fmt.Sprintf("%s %04d-%02d", s.AccountId, s.Year, *s.Month)
}
if s.Year > 0 {
return fmt.Sprintf("%s %04d", s.AccountId, s.Year)
}
}
if s.Month != nil {
return fmt.Sprintf("%04d-%02d", s.Year, *s.Month)
}
Expand Down Expand Up @@ -186,6 +210,20 @@ type QDevS3SliceParams struct {

var _ plugin.ToolLayerScope = (*QDevS3Slice)(nil)

func buildPrefixWithAccount(basePath string, accountId string, year int, month *int) string {
parts := splitPath(basePath)
if accountId != "" {
parts = append(parts, accountId)
}
if year > 0 {
parts = append(parts, fmt.Sprintf("%04d", year))
}
if month != nil {
parts = append(parts, fmt.Sprintf("%02d", *month))
}
return strings.Join(parts, "/")
}

func buildPrefix(basePath string, year int, month *int) string {
parts := splitPath(basePath)
if year > 0 {
Expand Down
Loading
Loading