Add stats to export operations (#4461)

Have a way to gather stats about the exported data.

Users can now call `ExportOperation.GetStats()` at the end of the run to get the stats for the operations. The data will be in the format `map[path.CategoryType]data.KindStats` whre `KindStats` is:

```go
type KindStats struct {
	BytesRead     int64
	ResourceCount int64
}
```
---

#### Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [x]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* https://github.com/alcionai/corso/issues/4311

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Abin Simon 2023-10-11 11:24:17 +05:30 committed by GitHub
parent 107b6883d5
commit 040257f8be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 255 additions and 13 deletions

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Skips graph calls for expired item download URLs. - Skips graph calls for expired item download URLs.
- Export operation now shows the stats at the end of the run
### Fixed ### Fixed
- Catch and report cases where a protected resource is locked out of access. SDK consumers have a new errs sentinel that allows them to check for this case. - Catch and report cases where a protected resource is locked out of access. SDK consumers have a new errs sentinel that allows them to check for this case.

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/dustin/go-humanize"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/alcionai/corso/src/cli/flags" "github.com/alcionai/corso/src/cli/flags"
@ -110,5 +111,14 @@ func runExport(
return Only(ctx, err) return Only(ctx, err)
} }
stats := eo.GetStats()
if len(stats) > 0 {
Infof(ctx, "\nExport details")
}
for k, s := range stats {
Infof(ctx, "%s: %d items (%s)", k.HumanString(), s.ResourceCount, humanize.Bytes(uint64(s.BytesRead)))
}
return nil return nil
} }

View File

@ -1,5 +1,12 @@
package data package data
import (
"io"
"sync/atomic"
"github.com/alcionai/corso/src/pkg/path"
)
type CollectionStats struct { type CollectionStats struct {
Folders, Folders,
Objects, Objects,
@ -15,3 +22,68 @@ func (cs CollectionStats) IsZero() bool {
func (cs CollectionStats) String() string { func (cs CollectionStats) String() string {
return cs.Details return cs.Details
} }
type KindStats struct {
BytesRead int64
ResourceCount int64
}
type ExportStats struct {
// data is kept private so that we can enforce atomic int updates
data map[path.CategoryType]KindStats
}
func (es *ExportStats) UpdateBytes(kind path.CategoryType, bytesRead int64) {
if es.data == nil {
es.data = map[path.CategoryType]KindStats{}
}
ks := es.data[kind]
atomic.AddInt64(&ks.BytesRead, bytesRead)
es.data[kind] = ks
}
func (es *ExportStats) UpdateResourceCount(kind path.CategoryType) {
if es.data == nil {
es.data = map[path.CategoryType]KindStats{}
}
ks := es.data[kind]
atomic.AddInt64(&ks.ResourceCount, 1)
es.data[kind] = ks
}
func (es *ExportStats) GetStats() map[path.CategoryType]KindStats {
return es.data
}
type statsReader struct {
io.ReadCloser
kind path.CategoryType
stats *ExportStats
}
func (sr *statsReader) Read(p []byte) (int, error) {
n, err := sr.ReadCloser.Read(p)
sr.stats.UpdateBytes(sr.kind, int64(n))
return n, err
}
// Create a function that will take a reader and return a reader that
// will update the stats
func ReaderWithStats(
reader io.ReadCloser,
kind path.CategoryType,
stats *ExportStats,
) io.ReadCloser {
if reader == nil {
return nil
}
return &statsReader{
ReadCloser: reader,
kind: kind,
stats: stats,
}
}

View File

@ -12,18 +12,21 @@ import (
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/export" "github.com/alcionai/corso/src/pkg/export"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
) )
func NewExportCollection( func NewExportCollection(
baseDir string, baseDir string,
backingCollection []data.RestoreCollection, backingCollection []data.RestoreCollection,
backupVersion int, backupVersion int,
stats *data.ExportStats,
) export.Collectioner { ) export.Collectioner {
return export.BaseCollection{ return export.BaseCollection{
BaseDir: baseDir, BaseDir: baseDir,
BackingCollection: backingCollection, BackingCollection: backingCollection,
BackupVersion: backupVersion, BackupVersion: backupVersion,
Stream: streamItems, Stream: streamItems,
Stats: stats,
} }
} }
@ -34,6 +37,7 @@ func streamItems(
backupVersion int, backupVersion int,
cec control.ExportConfig, cec control.ExportConfig,
ch chan<- export.Item, ch chan<- export.Item,
stats *data.ExportStats,
) { ) {
defer close(ch) defer close(ch)
@ -47,11 +51,22 @@ func streamItems(
} }
name, err := getItemName(ctx, itemUUID, backupVersion, rc) name, err := getItemName(ctx, itemUUID, backupVersion, rc)
if err != nil {
ch <- export.Item{
ID: itemUUID,
Error: err,
}
continue
}
stats.UpdateResourceCount(path.FilesCategory)
body := data.ReaderWithStats(item.ToReader(), path.FilesCategory, stats)
ch <- export.Item{ ch <- export.Item{
ID: itemUUID, ID: itemUUID,
Name: name, Name: name,
Body: item.ToReader(), Body: body,
Error: err, Error: err,
} }
} }

View File

@ -15,6 +15,7 @@ import (
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/export" "github.com/alcionai/corso/src/pkg/export"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
) )
@ -23,6 +24,7 @@ func NewExportCollection(
backingCollections []data.RestoreCollection, backingCollections []data.RestoreCollection,
backupVersion int, backupVersion int,
cec control.ExportConfig, cec control.ExportConfig,
stats *data.ExportStats,
) export.Collectioner { ) export.Collectioner {
return export.BaseCollection{ return export.BaseCollection{
BaseDir: baseDir, BaseDir: baseDir,
@ -30,6 +32,7 @@ func NewExportCollection(
BackupVersion: backupVersion, BackupVersion: backupVersion,
Cfg: cec, Cfg: cec,
Stream: streamItems, Stream: streamItems,
Stats: stats,
} }
} }
@ -40,6 +43,7 @@ func streamItems(
backupVersion int, backupVersion int,
cec control.ExportConfig, cec control.ExportConfig,
ch chan<- export.Item, ch chan<- export.Item,
stats *data.ExportStats,
) { ) {
defer close(ch) defer close(ch)
@ -54,6 +58,9 @@ func streamItems(
Error: err, Error: err,
} }
} else { } else {
stats.UpdateResourceCount(path.ChannelMessagesCategory)
body = data.ReaderWithStats(body, path.ChannelMessagesCategory, stats)
ch <- export.Item{ ch <- export.Item{
ID: item.ID(), ID: item.ID(),
// channel message items have no name // channel message items have no name

View File

@ -90,7 +90,8 @@ func (suite *ExportUnitSuite) TestStreamItems() {
[]data.RestoreCollection{test.backingColl}, []data.RestoreCollection{test.backingColl},
version.NoBackup, version.NoBackup,
control.DefaultExportConfig(), control.DefaultExportConfig(),
ch) ch,
&data.ExportStats{})
var ( var (
itm export.Item itm export.Item

View File

@ -27,6 +27,7 @@ func (ctrl *Controller) ProduceExportCollections(
exportCfg control.ExportConfig, exportCfg control.ExportConfig,
opts control.Options, opts control.Options,
dcs []data.RestoreCollection, dcs []data.RestoreCollection,
stats *data.ExportStats,
errs *fault.Bus, errs *fault.Bus,
) ([]export.Collectioner, error) { ) ([]export.Collectioner, error) {
ctx, end := diagnostics.Span(ctx, "m365:export") ctx, end := diagnostics.Span(ctx, "m365:export")
@ -51,6 +52,7 @@ func (ctrl *Controller) ProduceExportCollections(
opts, opts,
dcs, dcs,
deets, deets,
stats,
errs) errs)
case selectors.ServiceSharePoint: case selectors.ServiceSharePoint:
expCollections, err = sharepoint.ProduceExportCollections( expCollections, err = sharepoint.ProduceExportCollections(
@ -61,6 +63,7 @@ func (ctrl *Controller) ProduceExportCollections(
dcs, dcs,
ctrl.backupDriveIDNames, ctrl.backupDriveIDNames,
deets, deets,
stats,
errs) errs)
case selectors.ServiceGroups: case selectors.ServiceGroups:
expCollections, err = groups.ProduceExportCollections( expCollections, err = groups.ProduceExportCollections(
@ -72,6 +75,7 @@ func (ctrl *Controller) ProduceExportCollections(
ctrl.backupDriveIDNames, ctrl.backupDriveIDNames,
ctrl.backupSiteIDWebURL, ctrl.backupSiteIDWebURL,
deets, deets,
stats,
errs) errs)
default: default:

View File

@ -90,6 +90,7 @@ func (ctrl Controller) ProduceExportCollections(
_ control.ExportConfig, _ control.ExportConfig,
_ control.Options, _ control.Options,
_ []data.RestoreCollection, _ []data.RestoreCollection,
_ *data.ExportStats,
_ *fault.Bus, _ *fault.Bus,
) ([]export.Collectioner, error) { ) ([]export.Collectioner, error) {
return nil, ctrl.Err return nil, ctrl.Err

View File

@ -29,6 +29,7 @@ func ProduceExportCollections(
backupDriveIDNames idname.Cacher, backupDriveIDNames idname.Cacher,
backupSiteIDWebURL idname.Cacher, backupSiteIDWebURL idname.Cacher,
deets *details.Builder, deets *details.Builder,
stats *data.ExportStats,
errs *fault.Bus, errs *fault.Bus,
) ([]export.Collectioner, error) { ) ([]export.Collectioner, error) {
var ( var (
@ -52,7 +53,8 @@ func ProduceExportCollections(
path.Builder{}.Append(folders...).String(), path.Builder{}.Append(folders...).String(),
[]data.RestoreCollection{restoreColl}, []data.RestoreCollection{restoreColl},
backupVersion, backupVersion,
exportCfg) exportCfg,
stats)
case path.LibrariesCategory: case path.LibrariesCategory:
drivePath, err := path.ToDrivePath(restoreColl.FullPath()) drivePath, err := path.ToDrivePath(restoreColl.FullPath())
if err != nil { if err != nil {
@ -91,7 +93,8 @@ func ProduceExportCollections(
coll = drive.NewExportCollection( coll = drive.NewExportCollection(
baseDir.String(), baseDir.String(),
[]data.RestoreCollection{restoreColl}, []data.RestoreCollection{restoreColl},
backupVersion) backupVersion,
stats)
default: default:
el.AddRecoverable( el.AddRecoverable(
ctx, ctx,

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -64,8 +65,8 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
itemID = "itemID" itemID = "itemID"
containerName = "channelID" containerName = "channelID"
dii = groupMock.ItemInfo() dii = groupMock.ItemInfo()
body = io.NopCloser(bytes.NewBufferString( content = `{"displayname": "` + dii.Groups.ItemName + `"}`
`{"displayname": "` + dii.Groups.ItemName + `"}`)) body = io.NopCloser(bytes.NewBufferString(content))
exportCfg = control.ExportConfig{} exportCfg = control.ExportConfig{}
expectedPath = path.ChannelMessagesCategory.HumanString() + "/" + containerName expectedPath = path.ChannelMessagesCategory.HumanString() + "/" + containerName
expectedItems = []export.Item{ expectedItems = []export.Item{
@ -96,6 +97,8 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
}, },
} }
stats := data.ExportStats{}
ecs, err := ProduceExportCollections( ecs, err := ProduceExportCollections(
ctx, ctx,
int(version.Backup), int(version.Backup),
@ -105,6 +108,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
nil, nil,
nil, nil,
nil, nil,
&stats,
fault.New(true)) fault.New(true))
assert.NoError(t, err, "export collections error") assert.NoError(t, err, "export collections error")
assert.Len(t, ecs, 1, "num of collections") assert.Len(t, ecs, 1, "num of collections")
@ -113,7 +117,15 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
fitems := []export.Item{} fitems := []export.Item{}
size := 0
for item := range ecs[0].Items(ctx) { for item := range ecs[0].Items(ctx) {
b, err := io.ReadAll(item.Body)
assert.NoError(t, err, clues.ToCore(err))
// count up size for tests
size += len(b)
// have to nil out body, otherwise assert fails due to // have to nil out body, otherwise assert fails due to
// pointer memory location differences // pointer memory location differences
item.Body = nil item.Body = nil
@ -121,6 +133,11 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
} }
assert.Equal(t, expectedItems, fitems, "items") assert.Equal(t, expectedItems, fitems, "items")
expectedStats := data.ExportStats{}
expectedStats.UpdateBytes(path.ChannelMessagesCategory, int64(size))
expectedStats.UpdateResourceCount(path.ChannelMessagesCategory)
assert.Equal(t, expectedStats, stats, "stats")
} }
func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() { func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
@ -182,6 +199,8 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
}, },
} }
stats := data.ExportStats{}
ecs, err := ProduceExportCollections( ecs, err := ProduceExportCollections(
ctx, ctx,
int(version.Backup), int(version.Backup),
@ -191,6 +210,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
driveNameCache, driveNameCache,
siteWebURLCache, siteWebURLCache,
nil, nil,
&stats,
fault.New(true)) fault.New(true))
assert.NoError(t, err, "export collections error") assert.NoError(t, err, "export collections error")
assert.Len(t, ecs, 1, "num of collections") assert.Len(t, ecs, 1, "num of collections")
@ -199,9 +219,24 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
fitems := []export.Item{} fitems := []export.Item{}
size := 0
for item := range ecs[0].Items(ctx) { for item := range ecs[0].Items(ctx) {
// unwrap the body from stats reader
b, err := io.ReadAll(item.Body)
assert.NoError(t, err, clues.ToCore(err))
size += len(b)
bitem := io.NopCloser(bytes.NewBuffer(b))
item.Body = bitem
fitems = append(fitems, item) fitems = append(fitems, item)
} }
assert.Equal(t, expectedItems, fitems, "items") assert.Equal(t, expectedItems, fitems, "items")
expectedStats := data.ExportStats{}
expectedStats.UpdateBytes(path.FilesCategory, int64(size))
expectedStats.UpdateResourceCount(path.FilesCategory)
assert.Equal(t, expectedStats, stats, "stats")
} }

View File

@ -23,6 +23,7 @@ func ProduceExportCollections(
opts control.Options, opts control.Options,
dcs []data.RestoreCollection, dcs []data.RestoreCollection,
deets *details.Builder, deets *details.Builder,
stats *data.ExportStats,
errs *fault.Bus, errs *fault.Bus,
) ([]export.Collectioner, error) { ) ([]export.Collectioner, error) {
var ( var (
@ -43,7 +44,8 @@ func ProduceExportCollections(
drive.NewExportCollection( drive.NewExportCollection(
baseDir.String(), baseDir.String(),
[]data.RestoreCollection{dc}, []data.RestoreCollection{dc},
backupVersion)) backupVersion,
stats))
} }
return ec, el.Failure() return ec, el.Failure()

View File

@ -6,6 +6,7 @@ import (
"io" "io"
"testing" "testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -19,6 +20,7 @@ import (
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/export" "github.com/alcionai/corso/src/pkg/export"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
) )
type ExportUnitSuite struct { type ExportUnitSuite struct {
@ -245,15 +247,32 @@ func (suite *ExportUnitSuite) TestGetItems() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
stats := data.ExportStats{}
ec := drive.NewExportCollection( ec := drive.NewExportCollection(
"", "",
[]data.RestoreCollection{test.backingCollection}, []data.RestoreCollection{test.backingCollection},
test.version) test.version,
&stats)
items := ec.Items(ctx) items := ec.Items(ctx)
count := 0
size := 0
fitems := []export.Item{} fitems := []export.Item{}
for item := range items { for item := range items {
if item.Error == nil {
count++
}
if item.Body != nil {
b, err := io.ReadAll(item.Body)
assert.NoError(t, err, clues.ToCore(err))
size += len(b)
item.Body = io.NopCloser(bytes.NewBuffer(b))
}
fitems = append(fitems, item) fitems = append(fitems, item)
} }
@ -268,6 +287,19 @@ func (suite *ExportUnitSuite) TestGetItems() {
assert.Equal(t, test.expectedItems[i].Body, item.Body, "body") assert.Equal(t, test.expectedItems[i].Body, item.Body, "body")
assert.ErrorIs(t, item.Error, test.expectedItems[i].Error) assert.ErrorIs(t, item.Error, test.expectedItems[i].Error)
} }
var expectedStats data.ExportStats
if size+count > 0 { // it is only initialized if we have something
expectedStats = data.ExportStats{}
expectedStats.UpdateBytes(path.FilesCategory, int64(size))
for i := 0; i < count; i++ {
expectedStats.UpdateResourceCount(path.FilesCategory)
}
}
assert.Equal(t, expectedStats, stats, "stats")
}) })
} }
} }
@ -312,6 +344,8 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections() {
}, },
} }
stats := data.ExportStats{}
ecs, err := ProduceExportCollections( ecs, err := ProduceExportCollections(
ctx, ctx,
int(version.Backup), int(version.Backup),
@ -319,14 +353,30 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections() {
control.DefaultOptions(), control.DefaultOptions(),
dcs, dcs,
nil, nil,
&stats,
fault.New(true)) fault.New(true))
assert.NoError(t, err, "export collections error") assert.NoError(t, err, "export collections error")
assert.Len(t, ecs, 1, "num of collections") assert.Len(t, ecs, 1, "num of collections")
fitems := []export.Item{} fitems := []export.Item{}
size := 0
for item := range ecs[0].Items(ctx) { for item := range ecs[0].Items(ctx) {
// unwrap the body from stats reader
b, err := io.ReadAll(item.Body)
assert.NoError(t, err, clues.ToCore(err))
size += len(b)
bitem := io.NopCloser(bytes.NewBuffer(b))
item.Body = bitem
fitems = append(fitems, item) fitems = append(fitems, item)
} }
assert.Equal(t, expectedItems, fitems, "items") assert.Equal(t, expectedItems, fitems, "items")
expectedStats := data.ExportStats{}
expectedStats.UpdateBytes(path.FilesCategory, int64(size))
expectedStats.UpdateResourceCount(path.FilesCategory)
assert.Equal(t, expectedStats, stats, "stats")
} }

View File

@ -26,6 +26,7 @@ func ProduceExportCollections(
dcs []data.RestoreCollection, dcs []data.RestoreCollection,
backupDriveIDNames idname.CacheBuilder, backupDriveIDNames idname.CacheBuilder,
deets *details.Builder, deets *details.Builder,
stats *data.ExportStats,
errs *fault.Bus, errs *fault.Bus,
) ([]export.Collectioner, error) { ) ([]export.Collectioner, error) {
var ( var (
@ -56,7 +57,8 @@ func ProduceExportCollections(
drive.NewExportCollection( drive.NewExportCollection(
baseDir.String(), baseDir.String(),
[]data.RestoreCollection{dc}, []data.RestoreCollection{dc},
backupVersion)) backupVersion,
stats))
} }
return ec, el.Failure() return ec, el.Failure()

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -98,6 +99,8 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections() {
}, },
} }
stats := data.ExportStats{}
ecs, err := ProduceExportCollections( ecs, err := ProduceExportCollections(
ctx, ctx,
int(version.Backup), int(version.Backup),
@ -106,6 +109,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections() {
dcs, dcs,
cache, cache,
nil, nil,
&stats,
fault.New(true)) fault.New(true))
assert.NoError(t, err, "export collections error") assert.NoError(t, err, "export collections error")
assert.Len(t, ecs, 1, "num of collections") assert.Len(t, ecs, 1, "num of collections")
@ -113,9 +117,24 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections() {
assert.Equal(t, expectedPath, ecs[0].BasePath(), "base dir") assert.Equal(t, expectedPath, ecs[0].BasePath(), "base dir")
fitems := []export.Item{} fitems := []export.Item{}
size := 0
for item := range ecs[0].Items(ctx) { for item := range ecs[0].Items(ctx) {
// unwrap the body from stats reader
b, err := io.ReadAll(item.Body)
assert.NoError(t, err, clues.ToCore(err))
size += len(b)
bitem := io.NopCloser(bytes.NewBuffer(b))
item.Body = bitem
fitems = append(fitems, item) fitems = append(fitems, item)
} }
assert.Equal(t, expectedItems, fitems, "items") assert.Equal(t, expectedItems, fitems, "items")
expectedStats := data.ExportStats{}
expectedStats.UpdateBytes(path.FilesCategory, int64(size))
expectedStats.UpdateResourceCount(path.FilesCategory)
assert.Equal(t, expectedStats, stats, "stats")
} }

View File

@ -27,6 +27,7 @@ import (
"github.com/alcionai/corso/src/pkg/export" "github.com/alcionai/corso/src/pkg/export"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/store" "github.com/alcionai/corso/src/pkg/store"
) )
@ -46,6 +47,7 @@ type ExportOperation struct {
Selectors selectors.Selector Selectors selectors.Selector
ExportCfg control.ExportConfig ExportCfg control.ExportConfig
Version string Version string
stats data.ExportStats
acct account.Account acct account.Account
ec inject.ExportConsumer ec inject.ExportConsumer
@ -72,6 +74,7 @@ func NewExportOperation(
Selectors: sel, Selectors: sel,
Version: "v0", Version: "v0",
ec: ec, ec: ec,
stats: data.ExportStats{},
} }
if err := op.validate(); err != nil { if err := op.validate(); err != nil {
return ExportOperation{}, err return ExportOperation{}, err
@ -247,7 +250,7 @@ func (op *ExportOperation) do(
opStats.resourceCount = 1 opStats.resourceCount = 1
opStats.cs = dcs opStats.cs = dcs
expCollections, err := exportRestoreCollections( expCollections, err := produceExportCollections(
ctx, ctx,
op.ec, op.ec,
bup.Version, bup.Version,
@ -255,6 +258,9 @@ func (op *ExportOperation) do(
op.ExportCfg, op.ExportCfg,
op.Options, op.Options,
dcs, dcs,
// We also have opStats, but that tracks different data.
// Maybe we can look into merging them some time in the future.
&op.stats,
op.Errors) op.Errors)
if err != nil { if err != nil {
return nil, clues.Stack(err) return nil, clues.Stack(err)
@ -310,11 +316,19 @@ func (op *ExportOperation) finalizeMetrics(
return op.Errors.Failure() return op.Errors.Failure()
} }
// GetStats returns the stats of the export operation. You should only
// be calling this once the export collections have been read and process
// as the data that will be available here will be the data that was read
// and processed.
func (op *ExportOperation) GetStats() map[path.CategoryType]data.KindStats {
return op.stats.GetStats()
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Exporter funcs // Exporter funcs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
func exportRestoreCollections( func produceExportCollections(
ctx context.Context, ctx context.Context,
ec inject.ExportConsumer, ec inject.ExportConsumer,
backupVersion int, backupVersion int,
@ -322,6 +336,7 @@ func exportRestoreCollections(
exportCfg control.ExportConfig, exportCfg control.ExportConfig,
opts control.Options, opts control.Options,
dcs []data.RestoreCollection, dcs []data.RestoreCollection,
exportStats *data.ExportStats,
errs *fault.Bus, errs *fault.Bus,
) ([]export.Collectioner, error) { ) ([]export.Collectioner, error) {
complete := observe.MessageWithCompletion(ctx, "Preparing export") complete := observe.MessageWithCompletion(ctx, "Preparing export")
@ -337,6 +352,7 @@ func exportRestoreCollections(
exportCfg, exportCfg,
opts, opts,
dcs, dcs,
exportStats,
errs) errs)
if err != nil { if err != nil {
return nil, clues.Wrap(err, "exporting collections") return nil, clues.Wrap(err, "exporting collections")

View File

@ -88,6 +88,7 @@ type (
exportCfg control.ExportConfig, exportCfg control.ExportConfig,
opts control.Options, opts control.Options,
dcs []data.RestoreCollection, dcs []data.RestoreCollection,
stats *data.ExportStats,
errs *fault.Bus, errs *fault.Bus,
) ([]export.Collectioner, error) ) ([]export.Collectioner, error)

View File

@ -28,7 +28,8 @@ type itemStreamer func(
backingColls []data.RestoreCollection, backingColls []data.RestoreCollection,
backupVersion int, backupVersion int,
cfg control.ExportConfig, cfg control.ExportConfig,
ch chan<- Item) ch chan<- Item,
stats *data.ExportStats)
// BaseCollection holds the foundational details of an export collection. // BaseCollection holds the foundational details of an export collection.
type BaseCollection struct { type BaseCollection struct {
@ -45,6 +46,8 @@ type BaseCollection struct {
Cfg control.ExportConfig Cfg control.ExportConfig
Stream itemStreamer Stream itemStreamer
Stats *data.ExportStats
} }
func (bc BaseCollection) BasePath() string { func (bc BaseCollection) BasePath() string {
@ -53,7 +56,7 @@ func (bc BaseCollection) BasePath() string {
func (bc BaseCollection) Items(ctx context.Context) <-chan Item { func (bc BaseCollection) Items(ctx context.Context) <-chan Item {
ch := make(chan Item) ch := make(chan Item)
go bc.Stream(ctx, bc.BackingCollection, bc.BackupVersion, bc.Cfg, ch) go bc.Stream(ctx, bc.BackingCollection, bc.BackupVersion, bc.Cfg, ch, bc.Stats)
return ch return ch
} }