1

Dưới đây là một số thủ thuật để làm việc với trích xuất tệp nhật ký. Chúng tôi đang xem xét một số chiết xuất Enterprise Splunk. Chúng ta có thể nghịch ngợm với Splunk, cố gắng khám phá dữ liệu. Hoặc chúng ta có thể lấy một trích xuất đơn giản và tìm hiểu dữ liệu bằng Python.

Chạy các thử nghiệm khác nhau trong Python dường như hiệu quả hơn so với cố gắng thực hiện loại trò chơi thám hiểm này trong Splunk. Chủ yếu bởi vì không có bất kỳ ranh giới nào về những gì chúng ta có thể làm với dữ liệu. Chúng ta có thể tạo ra các mô hình thống kê rất tinh vi tất cả ở một nơi.

Về mặt lý thuyết, chúng ta có thể thực hiện rất nhiều cuộc thám hiểm trong Splunk. Nó có một loạt các tính năng báo cáo và phân tích.

Nhưng...

Sử dụng Splunk giả định chúng tôi biết những gì chúng tôi đang tìm kiếm. Trong nhiều trường hợp, chúng tôi không biết những gì chúng tôi đang tìm kiếm: chúng tôi đang khám phá. Chúng tôi có thể có một số dấu hiệu cho thấy một vài giao dịch API RESTful chậm, nhưng ít hơn thế. Làm thế nào chúng ta có thể tiến hành?

Bước một là lấy dữ liệu thô ở định dạng CSV. Giờ thì sao?

Đọc dữ liệu thô

Chúng ta sẽ bắt đầu bằng cách gói một đối tượng CSV.DictReader với một số chức năng bổ sung.

Những người theo chủ nghĩa hướng đối tượng sẽ phản đối chiến lược này. "Tại sao không mở rộng DictReader?" họ hỏi. Tôi không có câu trả lời tuyệt vời. Tôi nghiêng về lập trình chức năng và kết quả trực giao của các thành phần. Với cách tiếp cận OO hoàn toàn, chúng ta phải sử dụng các mixin có vẻ phức tạp hơn để đạt được điều này.

Khung chung của chúng tôi để xử lý nhật ký là thế này.

with open("somefile.csv") as source:
    rdr = csv.DictReader(source)
  

Điều này cho phép chúng tôi đọc trích xuất Splunk định dạng CSV. Chúng ta có thể lặp qua các hàng trong trình đọc. Đây là mẹo số 1. Nó không thực sự rất khó khăn, nhưng tôi thích nó.

with open("somefile.csv") as source:
    rdr = csv.DictReader(source)
    for row in rdr:
        print( "{host} {ResponseTime} {source} {Service}".format_map(row) )
  

Chúng tôi có thể - ở một mức độ hạn chế - báo cáo dữ liệu thô theo định dạng hữu ích. Nếu chúng ta muốn thay đổi đầu ra, chúng ta có thể thay đổi chuỗi định dạng. Có thể "{host: 30s} {ReponseTime: 8s} {source: s}" hoặc đại loại như thế.

Lọc

Một tình huống phổ biến là chúng tôi đã trích xuất quá nhiều và chỉ cần xem một tập hợp con. Chúng tôi có thể thay đổi bộ lọc Splunk, nhưng, chúng tôi ghét phải quá tải trước khi chúng tôi hoàn thành việc khám phá. Nó dễ dàng hơn nhiều để lọc trong Python. Khi chúng tôi đã học được những gì chúng tôi cần, chúng tôi có thể hoàn thiện trong Splunk.

with open("somefile.csv") as source:
    rdr = csv.DictReader(source)
    rdr_perf_log = (row for row in rdr if row['source'] == 'perf_log')
    for row in rdr_perf_log:
        print( "{host} {ResponseTime} {Service}".format_map(row) )
  

Chúng tôi đã chèn một biểu thức trình tạo sẽ lọc các hàng nguồn, cho phép chúng tôi làm việc với một tập hợp con có ý nghĩa.

Chiếu

Trong một số trường hợp, chúng tôi sẽ có thêm các cột dữ liệu nguồn mà chúng tôi không thực sự muốn sử dụng. Chúng tôi sẽ loại bỏ dữ liệu này bằng cách chiếu từng hàng.

Về nguyên tắc, Splunk không bao giờ tạo ra một cột trống. Tuy nhiên, nhật ký API RESTful có thể dẫn đến các tập dữ liệu với số lượng lớn các tiêu đề cột duy nhất dựa trên các khóa thay thế là một phần của URI yêu cầu. Các cột này sẽ có một hàng dữ liệu từ một yêu cầu sử dụng khóa thay thế đó. Đối với mỗi hàng khác, không có gì hữu ích trong cột đó. Cuộc sống sẽ đơn giản hơn nhiều nếu chúng ta loại bỏ các cột trống khỏi mỗi hàng.

Chúng ta cũng có thể làm điều này với một biểu thức trình tạo, nhưng nó hơi dài. Một chức năng tạo có phần dễ đọc hơn.

def project(reader):
    for row in reader:
        yield {k:v for k,v in row.items() if v}
  

Chúng tôi đã xây dựng một từ điển hàng mới từ một tập hợp con của các mục trong trình đọc ban đầu. Chúng tôi có thể sử dụng điều này để bọc đầu ra của bộ lọc của chúng tôi.

with open("somefile.csv") as source:
    rdr = csv.DictReader(source)
    rdr_perf_log = (row for row in rdr if row['source'] == 'perf_log')
    for row in project(rdr_perf_log):
        print( "{host} {ResponseTime} {Service}".format_map(row) )
  

Điều này sẽ giảm các cột không sử dụng có thể nhìn thấy trong phần bên trong của câu lệnh for.

Thay đổi ký hiệu

Ký hiệu hàng ['nguồn'] sẽ trở nên lộn xộn. Nó hoạt động tốt hơn với một kiểu.SimpleNamespace hơn là một từ điển. Điều này cho phép chúng tôi sử dụng row.source.

Đây là một mẹo hay để tạo ra thứ gì đó hữu ích hơn.

rdr_ns = (types.SimpleNamespace(**row) for row in reader)
    

Chúng ta có thể gấp nó thành chuỗi các bước như thế này.

with open("somefile.csv") as source:
    rdr = csv.DictReader(source)
    rdr_perf_log = (row for row in rdr if row['source'] == 'perf_log')
    rdr_proj = project(rdr_perf_log)
    rdr_ns = (types.SimpleNamespace(**row) for row in rdr_proj)
    for row in rdr_ns:
        print( "{host} {ResponseTime} {Service}".format_map(vars(row)) )
    

Lưu ý thay đổi nhỏ đối với phương thức format_map () của chúng tôi. Chúng tôi đã thêm hàm vars () để trích xuất từ ​​điển từ các thuộc tính của Không gian đơn giản.

Chúng ta có thể viết đây là một hàm để bảo toàn đối xứng cú pháp với các hàm khác.

def ns_reader(reader):
    return (types.SimpleNamespace(**row) for row in reader)
    

Thật vậy, chúng ta có thể viết điều này như một cấu trúc lambda được sử dụng như một hàm.

ns_reader = lambda reader: (types.SimpleNamespace(**row) for row in reader)
    

Trong khi hàm ns_reader () và lambda ns_reader () được sử dụng theo cùng một cách, thì việc viết một chuỗi tài liệu và kiểm tra đơn vị doctest cho lambda sẽ hơi khó hơn một chút. Vì lý do này, một lambda có lẽ nên tránh.

Chúng tôi có thể sử dụng bản đồ (hàng lambda: type.SimpleNamespace (** hàng), trình đọc). Một số người thích điều này hơn biểu thức máy phát điện.

Chúng ta có thể sử dụng một tuyên bố phù hợp với tuyên bố lợi suất nội bộ, nhưng dường như không có bất kỳ lợi ích nào từ việc đưa ra một tuyên bố lớn từ một điều nhỏ.

Chúng tôi có rất nhiều sự lựa chọn vì Python cung cấp rất nhiều tính năng lập trình chức năng. Chúng ta thường không thấy Python được chào mời như một ngôn ngữ chức năng. Tuy nhiên, chúng tôi có nhiều cách khác nhau để xử lý một ánh xạ đơn giản.

Ánh xạ: Chuyển đổi và dữ liệu phái sinh

Chúng ta thường có một danh sách các chuyển đổi dữ liệu khá rõ ràng. Ngoài ra, chúng tôi sẽ có một danh sách ngày càng tăng các mục dữ liệu có nguồn gốc. Các mục có nguồn gốc sẽ năng động và dựa trên các giả thuyết khác nhau mà chúng tôi đang thử nghiệm. Mỗi lần chúng tôi có một thử nghiệm hoặc câu hỏi, chúng tôi có thể thay đổi dữ liệu dẫn xuất.

Mỗi bước sau: lọc, chiếu, chuyển đổi và tạo đạo hàm, là các giai đoạn trong phần "bản đồ" của đường ống giảm bản đồ. Chúng ta có thể tạo một số hàm nhỏ hơn và áp dụng chúng với map (). Vì chúng tôi đang cập nhật một đối tượng có trạng thái, chúng tôi không thể sử dụng hàm map () chung. Nếu chúng tôi muốn đạt được một phong cách lập trình chức năng thuần túy hơn, chúng tôi sẽ sử dụng một tên miền bất biến thay vì một Không gian đơn giản có thể thay đổi.

def convert(reader):
    for row in reader:
        row._time = datetime.datetime.strptime(row.Time, "%Y-%m-%dT%H:%M:%S.%F%Z")
        row.response_time = float(row.ResponseTime)
        yield row
    

Khi chúng tôi khám phá, chúng tôi sẽ điều chỉnh phần thân của chức năng chuyển đổi này. Có lẽ chúng ta sẽ bắt đầu với một số bộ chuyển đổi và phái sinh tối thiểu. Chúng tôi sẽ mở rộng điều này với một số "có đúng không?" loại điều Chúng tôi sẽ đưa ra một số khi chúng tôi phát hiện ra rằng không hoạt động.

Xử lý tổng thể của chúng tôi trông như thế này:

with open("somefile.csv") as source:
    rdr = csv.DictReader(source)
    rdr_perf_log = (row for row in rdr if row['source'] == 'perf_log')
    rdr_proj = project(rdr_perf_log)
    rdr_ns = (types.SimpleNamespace(**row) for row in rdr_proj)
    rdr_converted = convert(rdr_ns)
    for row in rdr_converted:
        row.start_time = row._time - datetime.timedelta(seconds=row.response_time)
        row.service = some_mapping(row.Service)
        print( "{host:30s} {start_time:%H:%M:%S} {response_time:6.3f} {service}".format_map(vars(row)) )
    

Lưu ý rằng thay đổi trong cơ thể của tuyên bố của chúng tôi. Hàm convert () của chúng tôi tạo ra các giá trị mà chúng tôi chắc chắn. Chúng tôi đã thêm một số biến bổ sung bên trong vòng lặp for mà chúng tôi không chắc chắn 100%. Chúng tôi sẽ xem liệu chúng có hữu ích (hoặc thậm chí chính xác) trước khi cập nhật hàm convert () không.

Giảm

Khi nói đến việc giảm, chúng ta có thể áp dụng một phong cách xử lý hơi khác. Chúng ta cần cấu trúc lại ví dụ trước và biến nó thành hàm tạo.

def converted_log(some_file):
    with open(some_file) as source:
        rdr = csv.DictReader(source)
        rdr_perf_log = (row for row in rdr if row['source'] == 'perf_log')
        rdr_proj = project(rdr_perf_log)
        rdr_ns = (types.SimpleNamespace(**row) for row in rdr_proj)
        rdr_converted = convert(rdr_ns)
        for row in rdr_converted:
            row.start_time = row._time - datetime.timedelta(seconds=row.response_time)
            row.service = some_mapping(row.Service)
            yield row

Chúng tôi đã thay thế print () bằng một sản lượng.

Đây là một phần khác của tái cấu trúc này.

for row in converted_log("somefile.csv"):
    print( "{host:30s} {start_time:%H:%M:%S} {response_time:6.3f} {service}".format_map(vars(row)) )
    

Lý tưởng nhất, tất cả các chương trình của chúng tôi trông như thế này. Chúng tôi sử dụng chức năng tạo để tạo dữ liệu. Hiển thị cuối cùng của dữ liệu được giữ hoàn toàn riêng biệt. Điều này cho phép chúng tôi tái cấu trúc và thay đổi quá trình xử lý tự do hơn nhiều.

Bây giờ chúng ta có thể làm những việc như thu thập các hàng vào các đối tượng Counter () hoặc có thể tính toán một số thống kê. Chúng tôi có thể sử dụng defaultdict (danh sách) để nhóm các hàng theo dịch vụ.

by_service= defaultdict(list)
for row in converted_log("somefile.csv"):
    by_service[row.service] = row.response_time
for svc in sorted(by_service):
    m = statistics.mean( by_service[svc] )
    print( "{svc:15s} {m:.2f}".format_map(vars()) )
    

Chúng tôi đã quyết định tạo ra các đối tượng danh sách cụ thể ở đây. Chúng tôi có thể sử dụng itertools để nhóm thời gian phản hồi theo dịch vụ. Nó trông giống như lập trình chức năng phù hợp, nhưng việc triển khai chỉ ra một số hạn chế trong hình thức lập trình chức năng của Pythonic. Hoặc chúng ta phải sắp xếp dữ liệu (tạo một đối tượng danh sách) hoặc chúng ta phải tạo danh sách khi chúng ta nhóm dữ liệu. Để thực hiện một số thống kê khác nhau, việc nhóm dữ liệu thường dễ dàng hơn bằng cách tạo danh sách cụ thể.

Thay vì chỉ đơn giản là in một đối tượng hàng, chúng tôi hiện đang làm hai việc.

  1. Tạo một số biến cục bộ, như svc và m. Chúng ta có thể dễ dàng thêm phương sai hoặc các biện pháp khác.
  2. Sử dụng hàm vars () không có đối số, tạo ra một từ điển trong số các biến cục bộ.

Việc sử dụng vars () không có đối số - hoạt động như người địa phương () - là một mẹo hữu ích. Nó cho phép chúng tôi chỉ cần tạo bất kỳ biến cục bộ nào chúng tôi muốn và đưa chúng vào đầu ra được định dạng. Chúng tôi có thể hack nhiều loại biện pháp thống kê khác nhau mà chúng tôi nghĩ có thể có liên quan.

Bây giờ, vòng lặp xử lý thiết yếu của chúng tôi dành cho hàng trong convert_log ("somefile.csv"), chúng tôi có thể khám phá rất nhiều giải pháp thay thế xử lý trong một tập lệnh nhỏ, dễ sửa đổi. Chúng tôi có thể khám phá một số giả thuyết để xác định lý do tại sao một số giao dịch API RESTful chậm và các giao dịch khác lại nhanh. 

|