Separate ETW into low-level and high-level API

This commit is contained in:
Kevin Parsons
2018-12-21 16:18:22 -08:00
parent 7c26c75173
commit fac5ca0c3a
8 changed files with 253 additions and 125 deletions
+25 -10
View File
@@ -11,20 +11,35 @@ type EventData struct {
buffer bytes.Buffer
}
// NewEventData returns a new EventData with an empty buffer.
func NewEventData() *EventData {
return &EventData{}
// Bytes returns the raw binary data containing the event data. The returned
// value is not copied from the internal buffer, so it can be mutated by the
// EventData object after it is returned.
func (ed *EventData) Bytes() []byte {
return ed.buffer.Bytes()
}
// AddString appends the data for a string to the end of the buffer.
func (ed *EventData) AddString(data string) {
// WriteString appends a string, including the null terminator, to the buffer.
func (ed *EventData) WriteString(data string) {
ed.buffer.WriteString(data)
ed.buffer.WriteByte(0)
}
// This is mostly added for testing purposes, and will be removed later, as we
// shouldn't take a dependency on binary.Write knowing how to marshal values
// correctly for TraceLogging.
func (ed *EventData) AddSimple(data interface{}) {
binary.Write(&ed.buffer, binary.LittleEndian, data)
// WriteUint8 appends a uint8 to the buffer.
func (ed *EventData) WriteUint8(value uint8) {
ed.buffer.WriteByte(value)
}
// WriteUint16 appends a uint16 to the buffer.
func (ed *EventData) WriteUint16(value uint16) {
binary.Write(&ed.buffer, binary.LittleEndian, value)
}
// WriteUint32 appends a uint32 to the buffer.
func (ed *EventData) WriteUint32(value uint32) {
binary.Write(&ed.buffer, binary.LittleEndian, value)
}
// WriteUint64 appends a uint64 to the buffer.
func (ed *EventData) WriteUint64(value uint64) {
binary.Write(&ed.buffer, binary.LittleEndian, value)
}
@@ -27,23 +27,6 @@ const (
LevelVerbose
)
// Event represents a single ETW event. It can have field metadata and data
// added to it, and then be logged via a provider to actually send it to ETW.
type Event struct {
Descriptor *EventDescriptor
Metadata *EventMetadata
Data *EventData
}
// NewEvent returns a new instance of an event object.
func NewEvent(name string, descriptor *EventDescriptor) *Event {
return &Event{
Descriptor: descriptor,
Metadata: NewEventMetadata(name),
Data: &EventData{},
}
}
// EventDescriptor represents various metadata for an ETW event.
type EventDescriptor struct {
id uint16
@@ -61,13 +44,8 @@ func NewEventDescriptor() *EventDescriptor {
// Standard TraceLogging events default to the TraceLogging channel, and
// verbose level.
return &EventDescriptor{
id: 0,
version: 0,
Channel: ChannelTraceLogging,
Level: LevelVerbose,
Opcode: 0,
Task: 0,
Keyword: 0,
}
}
+49 -51
View File
@@ -82,15 +82,24 @@ type EventMetadata struct {
buffer bytes.Buffer
}
// NewEventMetadata returns a new EventMetadata with event name and initial
// metadata written to the buffer.
func NewEventMetadata(name string) *EventMetadata {
em := EventMetadata{}
// Bytes returns the raw binary data containing the event metadata. Before being
// returned, the current size of the buffer is written to the start of the
// buffer. The returned value is not copied from the internal buffer, so it can
// be mutated by the EventMetadata object after it is returned.
func (em *EventMetadata) Bytes() []byte {
// Finalize the event metadata buffer by filling in the buffer length at the
// beginning.
binary.LittleEndian.PutUint16(em.buffer.Bytes(), uint16(em.buffer.Len()))
return em.buffer.Bytes()
}
// WriteEventHeader writes the metadata for the start of an event to the buffer.
// This specifies the event name and tags.
func (em *EventMetadata) WriteEventHeader(name string, tags uint32) {
binary.Write(&em.buffer, binary.LittleEndian, uint16(0)) // Length placeholder
em.writeTags(0)
em.writeTags(tags)
em.buffer.WriteString(name)
em.buffer.WriteByte(0) // Null terminator for name
return &em
}
type field struct {
@@ -150,55 +159,44 @@ func (em *EventMetadata) writeTags(tags uint32) {
}
}
type fieldOpt func(f *field)
// WithOutType specifies the out type for the field. This value is used as a
// hint by the event decoder for how the field value should be formatted. If no
// out type is specified, a default formatting based on the in type will be
// used.
func WithOutType(outType OutType) fieldOpt {
return func(f *field) {
f.outType = outType
}
// WriteField writes the metadata for a simple field to the buffer.
func (em *EventMetadata) WriteField(name string, inType InType, outType OutType, tags uint32) {
em.writeField(field{
name: name,
inType: inType,
outType: outType,
tags: tags,
})
}
// WithTags adds a tag to the field. Tags are 28-bit values that have meaning
// only to the event consumer. The top 4 bits of the value will be ignored.
// Multiple uses of this option will cause the tags to be OR'd together.
func WithTags(tags uint32) fieldOpt {
return func(f *field) {
f.tags |= tags
}
// WriteArray writes the metadata for an array field to the buffer. The number
// of elements in the array must be written as a uint16 in the event data,
// immediately preceeding the event data.
func (em *EventMetadata) WriteArray(name string, inType InType, outType OutType, tags uint32) {
em.WriteField(name, inType|InTypeArray, outType, tags)
}
// WithCountedArray marks the field as being an array of a fixed number of
// elements. The number of elements is encoded directly into the field metadata.
func WithCountedArray(count uint16) fieldOpt {
return func(f *field) {
f.inType |= InTypeCountedArray
f.countedArraySize = count
}
// WriteCountedArray writes the metadata for an array field to the buffer. The
// size of a counted array is fixed, and the size is written into the metadata
// directly.
func (em *EventMetadata) WriteCountedArray(name string, count uint16, inType InType, outType OutType, tags uint32) {
em.writeField(field{
name: name,
inType: inType | InTypeCountedArray,
outType: outType,
tags: tags,
countedArraySize: count,
})
}
// WithArray marks the field as being an array of a dynamic number of elements.
// The number of elements must be written as a uint16 to the data block,
// immediately preceeding the array elements.
func WithArray() fieldOpt {
return func(f *field) {
f.inType |= InTypeArray
}
}
// AddField appends a single field to the end of the event metadata buffer.
func (em *EventMetadata) AddField(name string, inType InType, opts ...fieldOpt) {
f := field{
name: name,
inType: inType,
}
for _, opt := range opts {
opt(&f)
}
em.writeField(f)
// WriteStruct writes the metadata for a nested struct to the buffer. The struct
// contains the next N fields in the metadata, where N is specified by the
// fieldCount argument.
func (em *EventMetadata) WriteStruct(name string, fieldCount uint8, tags uint32) {
em.writeField(field{
name: name,
inType: InTypeStruct,
outType: OutType(fieldCount),
tags: tags,
})
}
+29
View File
@@ -0,0 +1,29 @@
package etw
// EventOpt defines the option function type that can be passed to
// Provider.WriteEvent to specify general event options, such as level and
// keyword.
type EventOpt func(*EventDescriptor, *uint32)
// WithLevel specifies the level of the event to be written.
func WithLevel(level Level) EventOpt {
return func(descriptor *EventDescriptor, tags *uint32) {
descriptor.Level = level
}
}
// WithKeyword specifies the keywords of the event to be written. Multiple uses
// of this option are OR'd together.
func WithKeyword(keyword uint64) EventOpt {
return func(descriptor *EventDescriptor, tags *uint32) {
descriptor.Keyword |= keyword
}
}
// WithTags specifies the tags of the event to be written. Tags is a 28-bit
// value (top 4 bits are ignored) which are interpreted by the event consumer.
func WithTags(newTags uint32) EventOpt {
return func(descriptor *EventDescriptor, tags *uint32) {
*tags |= newTags
}
}
+35
View File
@@ -0,0 +1,35 @@
package etw
// FieldOpt defines the option function type that can be passed to
// Provider.WriteEvent to add fields to the event.
type FieldOpt func(em *EventMetadata, ed *EventData)
// StringField adds a single string field to the event.
func StringField(name string, value string) FieldOpt {
return func(em *EventMetadata, ed *EventData) {
em.WriteField(name, InTypeANSIString, OutTypeUTF8, 0)
ed.WriteString(value)
}
}
// StringArray adds an array of strings to the event.
func StringArray(name string, values []string) FieldOpt {
return func(em *EventMetadata, ed *EventData) {
em.WriteArray(name, InTypeANSIString, OutTypeUTF8, 0)
ed.WriteUint16(uint16(len(values)))
for _, v := range values {
ed.WriteString(v)
}
}
}
// Struct adds a nested struct to the event, the FieldOpts in the opts argument
// are used to specify the fields of the struct.
func Struct(name string, opts ...FieldOpt) FieldOpt {
return func(em *EventMetadata, ed *EventData) {
em.WriteStruct(name, uint8(len(opts)), 0)
for _, opt := range opts {
opt(em, ed)
}
}
}
+56 -11
View File
@@ -172,6 +172,8 @@ func providerIDFromName(name string) (*windows.GUID, error) {
}, nil
}
// NewProvider creates and registers a new ETW provider. The provider ID is
// generated based on the provider name.
func NewProvider(name string, callback EnableCallback) (provider *Provider, err error) {
id, err := providerIDFromName(name)
if err != nil {
@@ -180,7 +182,10 @@ func NewProvider(name string, callback EnableCallback) (provider *Provider, err
return NewProviderWithID(name, id, callback)
}
// NewProvider creates and registers a new provider.
// NewProviderWithID creates and registers a new ETW provider, allowing the
// provider ID to be manually specified. This is most useful when there is an
// existing provider ID that must be used to conform to existing diagnostic
// infrastructure.
func NewProviderWithID(name string, id *windows.GUID, callback EnableCallback) (provider *Provider, err error) {
provider = providers.newProvider()
defer func() {
@@ -246,16 +251,56 @@ func (provider *Provider) IsEnabledForLevelAndKeywords(level Level, keywords uin
return true
}
// WriteEvent writes a single event to ETW, from this provider.
func (provider *Provider) WriteEvent(event *Event) error {
// Finalize the event metadata buffer by filling in the buffer length at the
// beginning.
binary.LittleEndian.PutUint16(event.Metadata.buffer.Bytes(), uint16(event.Metadata.buffer.Len()))
// WriteEvent writes a single ETW event from the provider. The event is
// constructed based on the EventOpt and FieldOpt values that are passed as
// opts.
func (provider *Provider) WriteEvent(name string, opts ...interface{}) error {
tags := uint32(0)
descriptor := NewEventDescriptor()
em := &EventMetadata{}
ed := &EventData{}
var dataDescriptors [3]eventDataDescriptor
dataDescriptors[0].set(eventDataDescriptorTypeProviderMetadata, provider.metadata)
dataDescriptors[1].set(eventDataDescriptorTypeEventMetadata, event.Metadata.buffer.Bytes())
dataDescriptors[2].set(eventDataDescriptorTypeUserData, event.Data.buffer.Bytes())
// We need to evaluate the EventOpts first since they might change tags, and
// we write out the tags before evaluating FieldOpts.
for _, opt := range opts {
if v, ok := opt.(EventOpt); ok {
v(descriptor, &tags)
}
}
return eventWriteTransfer(provider.handle, event.Descriptor, nil, nil, 3, &dataDescriptors[0])
em.WriteEventHeader(name, tags)
for _, opt := range opts {
if v, ok := opt.(FieldOpt); ok {
v(em, ed)
}
}
return provider.WriteEventRaw(descriptor, [][]byte{em.Bytes()}, [][]byte{ed.Bytes()})
}
// WriteEventRaw writes a single ETW event from the provider. This function is
// less abstracted than WriteEvent, and presents a fairly direct interface to
// the event writing functionality. It expects a series of event metadata and
// event data blobs to be passed in, which must conform to the TraceLogging
// schema. The functions on EventMetadata and EventData can help with creating
// these blobs. The blobs of each type are effectively concatenated together by
// the ETW infrastructure.
func (provider *Provider) WriteEventRaw(descriptor *EventDescriptor, metadataBlobs [][]byte, dataBlobs [][]byte) error {
dataDescriptorCount := uint32(1 + len(metadataBlobs) + len(dataBlobs))
dataDescriptors := make([]eventDataDescriptor, dataDescriptorCount)
i := 0
dataDescriptors[i].set(eventDataDescriptorTypeProviderMetadata, provider.metadata)
i++
for _, blob := range metadataBlobs {
dataDescriptors[i].set(eventDataDescriptorTypeEventMetadata, blob)
i++
}
for _, blob := range dataBlobs {
dataDescriptors[i].set(eventDataDescriptorTypeUserData, blob)
i++
}
return eventWriteTransfer(provider.handle, descriptor, nil, nil, dataDescriptorCount, &dataDescriptors[0])
}
+47 -20
View File
@@ -51,29 +51,56 @@ func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Println("Press enter to log an event")
fmt.Println("Press enter to log events")
reader.ReadString('\n')
event := etw.NewEvent("TestEvent", etw.NewEventDescriptor())
event.Metadata.AddField("TestField", etw.InTypeANSIString)
event.Data.AddString("Foo")
event.Metadata.AddField("TestField2", etw.InTypeANSIString)
event.Data.AddString("Bar")
event.Metadata.AddField("TestArray", etw.InTypeANSIString, etw.WithArray())
event.Data.AddSimple(uint16(5))
event.Data.AddString("Item1")
event.Data.AddString("Item2")
event.Data.AddString("Item3")
event.Data.AddString("Item4")
event.Data.AddString("Item5")
if err := provider.WriteEvent(event); err != nil {
fmt.Println(err)
// Write using high-level API.
if err := provider.WriteEvent(
"TestEvent",
etw.WithLevel(etw.LevelInfo),
etw.WithKeyword(0x140),
etw.StringField("TestField", "Foo"),
etw.StringField("TestField2", "Bar"),
etw.Struct("TestStruct",
etw.StringField("Field1", "Value1"),
etw.StringField("Field2", "Value2")),
etw.StringArray("TestArray", []string{
"Item1",
"Item2",
"Item3",
"Item4",
"Item5",
}),
); err != nil {
logrus.Error(err)
return
}
fmt.Println("Event written")
fmt.Println("Press enter to exit")
reader.ReadString('\n')
// Write using low-level API.
descriptor := etw.NewEventDescriptor()
descriptor.Level = etw.LevelInfo
descriptor.Keyword = 0x140
em := &etw.EventMetadata{}
ed := &etw.EventData{}
em.WriteEventHeader("TestEvent", 0)
em.WriteField("TestField", etw.InTypeANSIString, etw.OutTypeUTF8, 0)
ed.WriteString("Foo")
em.WriteField("TestField2", etw.InTypeANSIString, etw.OutTypeUTF8, 0)
ed.WriteString("Bar")
em.WriteStruct("TestStruct", 2, 0)
em.WriteField("Field1", etw.InTypeANSIString, etw.OutTypeUTF8, 0)
ed.WriteString("Value1")
em.WriteField("Field2", etw.InTypeANSIString, etw.OutTypeUTF8, 0)
ed.WriteString("Value2")
em.WriteArray("TestArray", etw.InTypeANSIString, etw.OutTypeDefault, 0)
ed.WriteUint16(5)
ed.WriteString("Item1")
ed.WriteString("Item2")
ed.WriteString("Item3")
ed.WriteString("Item4")
ed.WriteString("Item5")
if err := provider.WriteEventRaw(descriptor, [][]byte{em.Bytes()}, [][]byte{ed.Bytes()}); err != nil {
logrus.Error(err)
return
}
}
+12 -11
View File
@@ -46,30 +46,31 @@ func (h *Hook) Fire(e *logrus.Entry) error {
return nil
}
descriptor := etw.NewEventDescriptor()
opts := make([]interface{}, len(e.Data))
i := 0
// We could try to map Logrus levels to ETW levels, but we would lose some
// fidelity as there are fewer ETW levels. So instead we use the level
// directly.
descriptor.Level = etw.Level(e.Level)
opts[i] = etw.WithLevel(etw.Level(e.Level))
i++
event := etw.NewEvent("LogrusEntry", descriptor)
event.Metadata.AddField("Message", etw.InTypeANSIString, etw.WithOutType(etw.OutTypeUTF8))
event.Data.AddString(e.Message)
opts[i] = etw.StringField("Message", e.Message)
i++
for k, v := range e.Data {
switch v := v.(type) {
case string:
event.Metadata.AddField(k, etw.InTypeANSIString, etw.WithOutType(etw.OutTypeUTF8))
event.Data.AddString(v)
opts[i] = etw.StringField(k, v)
default:
event.Metadata.AddField(k, etw.InTypeANSIString, etw.WithOutType(etw.OutTypeUTF8))
event.Data.AddString(fmt.Sprintf("<unknown type: %v> %v", reflect.TypeOf(v), v))
opts[i] = etw.StringField(k, fmt.Sprintf("<unknown type: %v> %v", reflect.TypeOf(v), v))
}
i++
}
h.provider.WriteEvent(event)
h.provider.WriteEvent(
"LogrusEntry",
opts...)
return nil
}